Представление Recycler + MQTT иногда не перерисовывается после изменения набора данных - PullRequest
0 голосов
/ 08 мая 2020

Я боролся с проблемой перерисовки экрана для RecylerView, когда моя базовая модель была изменена другим потоком. Но у меня закончились идеи.

Мое приложение получает сообщения от MQTT topi c и отображает их в RecyclerView как своего рода "историю" или "журнал".

Это работает нормально, пока сеанс MQTT не подключается автоматически.

После повторного подключения сеанса MQTT после разорванного соединения я все еще получаю сообщения от MQTT topi c, сообщения все еще добавляются в мою модель, Я все еще вызываю уведомление «Данные изменены», я по-прежнему аннулирую элемент управления RecyclerView, но RecyclerView больше не перекрашивается, чтобы отобразить новое сообщение на экране.

Если я вручную принудительно обновляю / перекрашиваю экран ( например, прокрутите представление ресайклера, переключитесь на другое приложение и снова и т.д. заставлять его не перерисовываться, когда базовая модель изменяется в результате сообщений, полученных от MQTT topi c, но только если сеанс MQTT сброшен и переподключен ?????

И, очевидно, что мне нужно сделать, чтобы это исправить ?????

Обновить

Я пробовал добавить следующий метод (который активируется onClick плавающей кнопки).

    public void buttonClick (View v) {
        mAdapter.add("Button Message");
        Toast.makeText(getApplicationContext(),"Button message added", Toast.LENGTH_SHORT).show();
    }

Этот метод страдает от та же проблема, что и сообщения полученные от MQTT topi c. Если я нажму на перед автопереподключение MQTT, в мой RecyclerView добавится и отобразится «Сообщение кнопки». "Сообщение кнопки" метода больше не отображается, если я не принудительно обновлю sh RecyclerList. FWIW, всегда отображается «Toast» (до и после автопереключения MQTT).

Может быть, я наткнулся на какую-то странную ошибку в RecyclerView ???

FWIW 1, я прочитал много сообщений, пытающихся заставить RecyclerView работать в отношении обновлений фоновых потоков базовой модели данных. Некоторые предлагают запустить уведомление в потоке MainUI. Я считаю, что это имеет значение. Раньше он никогда не отображал какие-либо сообщения, полученные от MQTT topi c, теперь это отображается, но не в случае потери и повторного подключения соединения.

FWIW 2, я знаю, что notifyDataSetChanged следует использовать как последнее средство, поскольку оно наименее эффективно. Я пробовал другие способы уведомления. Эти другие методы уведомления создают ту же проблему, что и notifyDataSetChanged. На данный момент я пытаюсь сделать это простым, чтобы заставить его работать. Теперь я могу сосредоточиться на эффективности.

Вот соответствующие фрагменты кода.

Во-первых, обратный вызов MQTT, который вызывается при получении сообщения:

        @Override
        public void messageArrived(final String topic, MqttMessage message) throws Exception {
            final String msg = new String(message.getPayload());
            if ("glennm/test/temp".equals(topic)) {
                Log.i("RCVD", "Temperature: " + msg);   // This code is shown to illustrate a control that
                if (textViewTemperature != null) {      // always seems to be redisplayed when a message is received
                    textViewTemperature.setText(msg + "°");  // even if the connection is lost and reconnected
                    textViewTemperature.invalidate();
                    temperatureHistory.add(msg);
                    temperatureHistory.dump("TEMP");
                } else {
                    Log.e("NULL", "textView temperature control is null");
                }
            } else if ("glennm/test/humid".equals(topic)) {
// Code that updates the humidity text view omitted for brevity (as it is basically the same as the temperature code above.
            } else {            /***** This is the problem area - other messages logged to the Recycler view ****/
                String wrk = topic;
                if (topic != null && topic.toLowerCase().startsWith("glennm/test")) {
                    wrk = topic.substring(12);
                }
                final String topicToShow = wrk;
                textViewOtherTopic.setText(topicToShow);
                textViewOtherMessage.setText(msg);

//                mAdapter.add(topicToShow + ": " + msg);

                // The notify that the add method calls ***MUST*** be run on the main UI thread.
                // Failure to do so means that the call will sometimes be ignored and the
                // Recycler view is not updated to show the new incoming value.
                // https://stackoverflow.com/questions/36467236/notifydatasetchanged-recyclerview-is-it-an-asynchronous-call/36512407#36512407
                // This seems to help, but we still seem to have the same behaviour if the MQTT connection resets.
                runOnUiThread(new Runnable() {
//                recyclerView.post(new Runnable() {
                    @Override
                    public void run() {
                        mAdapter.add(topicToShow + ": " + msg);
                        recyclerView.invalidate();
                    }
                });
                Log.i("RCVD", "Other Topic: " + topic + ", message: " + msg);
            }
        }
    }

Во-вторых, код, который вызывается для добавить сообщение в базовую модель данных (и уведомить пользовательский интерфейс о его перерисовке).

  public void add(String msg) {
    Log.d("HISTORY", "Adding message: " + msg);
    messageList.add(msg);
    while (messageList.size() > MAX_HISTORY) {
      messageList.remove(0);
    }
    Log.d("HISTORY", "Notifying data set changed");
    _parent.runOnUiThread(new Runnable () {
      @Override
      public void run() {
        notifyDataSetChanged();
      }
    });
    Log.d("HISTORY", "Notifying data set changed - complete");
  }

Наконец, вот три снимка экрана, которые пытаются проиллюстрировать проблему. В первом случае сообщения были получены от MQTT topi c и отображаются как в журнале , так и в поле «текущее сообщение» (элементы управления textViewOtherTopi c и textViewOtherMessage), расположенные ниже уровня влажности.

Message displays in both

Что происходит между первым и вторым снимком экрана, так это то, что служба MQTT потеряла соединение и автоматически переподключилась. После этого полученное сообщение отображается только в представлении «текущее сообщение», а не в представлении ресайклера «журнала сообщений» (несмотря на добавление в модель).

after MQTT connection reset, messages only appear in

Отсутствующие сообщения отображаются только тогда, когда окно Recycler View принудительно перекрашивается внешним (ручным) действием пользователя (например, прокруткой).

Manual user action causing screen repaint causes

Вот пример команды, использующей mqtt-клиент mosquitto, который используется для отправки сообщения в приложение:

mosquitto_pub -h test.mosquitto.org -t "glennm/test/comment" -q 1 -m "It's a new day, but still cold! 3"

Ниже приведен полный код для двух классов (включая многие из комментированных из моих попыток) ...

Основное действие:

package com.gtajb.tempmonitorapp;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import org.eclipse.paho.android.service.MqttAndroidClient;
import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
import org.eclipse.paho.client.mqttv3.IMqttToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;

import java.util.ArrayList;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {
    private MqttAndroidClient mqttAndroidClient;

    private String serverUri = "tcp://test.mosquitto.org:1883";
    public static final String clientId = UUID.randomUUID().toString();

    public final String subscriptionTopic = "glennm/test/#";
    public final String publishTopic = "glennm/test/tome";
    public final String publishMessage = "Hello from Android test client";

    private TextView textViewTemperature;
    private TextView textViewHumidity;
    private TextView textViewOtherTopic;
    private TextView textViewOtherMessage;

    private MessageCallBack messageCallBack = new MessageCallBack();
    private RecyclerView recyclerView;
    private MessageHistory mAdapter;
    private RecyclerView.LayoutManager layoutManager;

    private ReadingsHistory temperatureHistory = new ReadingsHistory();
    private ReadingsHistory humidityHistory = new ReadingsHistory();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d("INFO", "Createing the mqtt client with client ID: " + clientId);
        mqttAndroidClient = new MqttAndroidClient(getApplicationContext(), serverUri, clientId);
        mqttAndroidClient.setCallback(new MqttClientCallback());

        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
        mqttConnectOptions.setAutomaticReconnect(true);
        mqttConnectOptions.setCleanSession(false);

        textViewTemperature = findViewById(R.id.temperatureValueLabel);
        textViewTemperature.setText("Temp goes here");
        textViewHumidity = findViewById(R.id.humidtyValueLabel);
        textViewHumidity.setText("Humid goes here");

        textViewOtherTopic = findViewById(R.id.otherTopicValueLabel);
        textViewOtherMessage = findViewById(R.id.otherMessageValueLabel);
        textViewOtherTopic.setText(".");
        textViewOtherMessage.setText(".");

        recyclerView = findViewById(R.id.historyPanel);
        mAdapter = new MessageHistory(new ArrayList<String>(), this, recyclerView);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(mAdapter);

        mAdapter.add("A test message");
        messageCallBack = new MessageCallBack();

        try {
            mqttAndroidClient.connect(mqttConnectOptions, null, new IMqttActionListener() {
                @Override
                public void onSuccess(IMqttToken asyncActionToken) {
                    DisconnectedBufferOptions disconnectedBufferOptions = new DisconnectedBufferOptions();
                    disconnectedBufferOptions.setBufferEnabled(true);
                    disconnectedBufferOptions.setBufferSize(100);
                    disconnectedBufferOptions.setPersistBuffer(false);
                    disconnectedBufferOptions.setDeleteOldestMessages(false);
                    mqttAndroidClient.setBufferOpts(disconnectedBufferOptions);
                    //subscribeToTopic();
                }

                @Override
                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
                    Log.e("CONNECT", "Failed to connect to " + serverUri);
                    Log.e("CONNECT", exception.getMessage());
                }
            });
        } catch (MqttException e) {
            Log.e("CONNECT", "Exception connecting to " + serverUri);
            Log.e("CONNECT", e.getMessage());
            Log.e("CONNECT", Log.getStackTraceString(e));
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i("CONN", "Closing MQTT connection");
        mqttAndroidClient.close();
    }



    public class MqttClientCallback implements MqttCallbackExtended {
        @Override
        public void connectComplete(boolean reconnect, String serverURI) {
            if (reconnect) {
                Log.i("CONN", "Reconnected to: " + serverURI);
                subscribeToTopic();
            } else {
                Log.i("CONN", "Connected to: " + serverURI);
                subscribeToTopic();
            }
        }

        @Override
        public void connectionLost(Throwable cause) {
            Log.i("CONN", "Connection lost");
        }

        @Override
        public void messageArrived(String topic, MqttMessage message) throws Exception {
            Log.i("MSG", topic + " - " + new String(message.getPayload()));
        }

        @Override
        public void deliveryComplete(IMqttDeliveryToken token) {
            Log.i("PUB", "Delivery complete");
        }
    }


    public void subscribeToTopic() {
        try {
            Log.i("TOPIC", "Subscribing to: " + subscriptionTopic);
//            mqttAndroidClient.subscribe(subscriptionTopic, 0, null, new IMqttActionListener() {
//                @Override
//                public void onSuccess(IMqttToken asyncActionToken) {
//                    Log.i("SUBS", "Subscription to " + subscriptionTopic + " on " + serverUri + " successful");
//                }
//
//                @Override
//                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
//                    Log.i("SUBS", "Subscription to " + subscriptionTopic + " on " + serverUri + " FAILED");
//                }
//            });
            mqttAndroidClient.subscribe(subscriptionTopic, 0,  messageCallBack);
        } catch (MqttException e) {
            Log.e("SUBS", "Failed to subscribe to topic: " + subscriptionTopic + " on " + serverUri);
            Log.e("SUBS", e.getMessage());
        }
    }

    public void setTemperatureValue(String val) {
        textViewTemperature.setText(val);
    }


    public void setHumidityValue(String val) {
        textViewHumidity.setText(val);
    }


    public class MessageCallBack implements IMqttMessageListener , IMqttActionListener {

        @Override
        public void onSuccess(IMqttToken asyncActionToken) {
            Log.i("MQTT", "Successful operation " + asyncActionToken.toString());
            textViewTemperature.setText("Subscribed");
        }

        @Override
        public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
            Log.i("MQTT", "Un Successful operation + " + asyncActionToken.toString());
            textViewTemperature.setText("Not Subscribed");
        }

        @Override
        public void messageArrived(final String topic, MqttMessage message) throws Exception {
            final String msg = new String(message.getPayload());
            if ("glennm/test/temp".equals(topic)) {
                Log.i("RCVD", "Temperature: " + msg);
                if (textViewTemperature != null) {
                    textViewTemperature.setText(msg + "°");
                    textViewTemperature.invalidate();
                    temperatureHistory.add(msg);
                    temperatureHistory.dump("TEMP");
                } else {
                    Log.e("NULL", "textView temperature control is null");
                }
            } else if ("glennm/test/humid".equals(topic)) {
                Log.i("RCVD", "Humidity: " + msg);
                textViewHumidity.setText(msg + "%");
                textViewHumidity.invalidate();
                humidityHistory.add(msg);
                humidityHistory.dump("HUMID");
            } else {
                String wrk = topic;
                if (topic != null && topic.toLowerCase().startsWith("glennm/test")) {
                    wrk = topic.substring(12);
                }
                final String topicToShow = wrk;
                textViewOtherTopic.setText(topicToShow);
                textViewOtherMessage.setText(msg);

//                mAdapter.add(topicToShow + ": " + msg);

                // The notify that the add method calls ***MUST*** be run on the main UI thread.
                // Failure to do so means that the call will sometimes be ignored and the
                // Recycler view is not updated to show the new incoming value.
                // https://stackoverflow.com/questions/36467236/notifydatasetchanged-recyclerview-is-it-an-asynchronous-call/36512407#36512407
                // This seems to help, but we still seem to have the same behaviour.
                runOnUiThread(new Runnable() {
//                recyclerView.post(new Runnable() {
                    @Override
                    public void run() {
                        mAdapter.add(topicToShow + ": " + msg);
//                        recyclerView.invalidate();
                    }
                });
                Log.i("RCVD", "Other Topic: " + topic + ", message: " + msg);
//                Context context = getApplicationContext();
//                Toast msgPopup = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
//                msgPopup.show();
            }
        }
    }
}

Класс истории сообщений:

package com.gtajb.tempmonitorapp;

import android.app.Activity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;

public class MessageHistory extends RecyclerView.Adapter<MessageHistory.Callback> {

    private ArrayList<String> messageList = new ArrayList();
    public static final int MAX_HISTORY = 100;
    private Activity _parent;
    private RecyclerView rv;

    public class Callback extends RecyclerView.ViewHolder {
        TextView mTextView;

        Callback(View itemView) {
            super(itemView);
            mTextView = itemView.findViewById(R.id.row_text);
        }
    }

    public MessageHistory(ArrayList<String> messageList, Activity parent, RecyclerView rv) {
        super();
        this.messageList = messageList;
        this._parent = parent;
        this.rv = rv;
        add("Test Message 1");
        add("Test Message 2");
    }

    @NonNull
    @Override
    public Callback onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.history_row, parent, false);
        return new Callback(v);
    }

    @Override
    public void onBindViewHolder(@NonNull Callback holder, int position) {
        Log.d("HISTORY", "Setting " + position + ": " + messageList.get(position));
        holder.mTextView.setText(messageList.get(position));
    }

    @Override
    public int getItemCount() {
        Log.d("HISTORY", "Message Count: " + messageList.size());
        return messageList.size();
    }

    /**
     * Add a message to the message log.
     * @param msg the message to add.
     */
    public void add(String msg) {
        Log.d("HISTORY", "Adding message: " + msg);
        messageList.add(msg);
        while (messageList.size() > MAX_HISTORY) {
            messageList.remove(0);
        }
//        getItemCount();
        Log.d("HISTORY", "Notifying data set changed");
        _parent.runOnUiThread(new Runnable () {
            @Override
            public void run() {
                notifyDataSetChanged();
            }
        });
        //this.notifyDataSetChanged();
        Log.d("HISTORY", "Notifying data set changed - complete");

//        rv.invalidate();
//        rv.refreshDrawableState();
//        this.notifyItemInserted(messageList.size());
//        final RecyclerView.Adapter adapter = this;
//        _parent.runOnUiThread(new Runnable() {
//            @Override
//            public void run() {
//                adapter.notifyDataSetChanged();
//            }
//        });
    }
}
...