Я боролся с проблемой перерисовки экрана для 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), расположенные ниже уровня влажности.
Что происходит между первым и вторым снимком экрана, так это то, что служба MQTT потеряла соединение и автоматически переподключилась. После этого полученное сообщение отображается только в представлении «текущее сообщение», а не в представлении ресайклера «журнала сообщений» (несмотря на добавление в модель).
Отсутствующие сообщения отображаются только тогда, когда окно Recycler View принудительно перекрашивается внешним (ручным) действием пользователя (например, прокруткой).
Вот пример команды, использующей 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();
// }
// });
}
}