When I receive a lot of push messages (let's say 50) from GCM within 1 second I receive the following exception:
java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(2131427434, class android.widget.ListView) with Adapter(class a.n)] at android.widget.ListView.layoutChildren(ListView.java:1544) at android.widget.AbsListView.onLayout(AbsListView.java:2045) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v4.view.ViewPager.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v4.widget.DrawerLayout.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.support.v7.internal.widget.ActionBarOverlayLayout.onLayout(Unknown Source) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1670) at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1528) at android.widget.LinearLayout.onLayout(LinearLayout.java:1441) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.widget.FrameLayout.onLayout(FrameLayout.java:446) at android.view.View.layout(View.java:14255) at android.view.ViewGroup.layout(ViewGroup.java:4413) at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1998) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1812) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1050) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4560) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749) at android.view.Choreographer.doCallbacks(Choreographer.java:562) at android.view.Choreographer.doFrame(Choreographer.java:532) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735) at android.os.Handler.handleCallback(Handler.java:725) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:137) at android.app.ActivityThread.main(ActivityThread.java:5171) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:511) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:797) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:564) at dalvik.system.NativeStart.main(Native Method)
I have already tried to fix this, by putting BOTH the messages.add()
and notifyDataSetChanged()
inside the runOnUIThread
. I guess that happens because onUpdate()
of my listener is called for every push message. But shouldn't that problem be solved by runOnUIThread()
, because everything is executed consecutively?
MainApplication app = (MainApplication) context.getApplicationContext();
app.setOnRoomMessageUpdateListener(new OnRoomMessageUpdateListener() {
@Override
public void onUpdate() {
// save message with highest time, so we can only query the new
// messages
long highestTime = getHighestMessageTime();
messageDatabase.getConditionBuilder().add(
DatabaseHelper.KEY_MESSAGE_ROOM_ID + " = ? AND " + DatabaseHelper.KEY_MESSAGE_LOCAL_TIME
+ " > ? AND " + DatabaseHelper.MESSAGE_TABLE_NAME + "."
+ DatabaseHelper.KEY_MESSAGE_USER_ID + " <> ?",
new String[] { String.valueOf(roomID), String.valueOf(highestTime),
String.valueOf(user.getUserID()) });
messageDatabase.getConditionBuilder().setSortOrder(DatabaseHelper.KEY_MESSAGE_LOCAL_TIME + " DESC");
final ArrayList<Message> newMessages = messageDatabase.getList();
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
messages.addAll(newMessages);
messageAdapter.notifyDataSetChanged();
}
});
}
});
EDIT: I probably missed out a very important part of the code, which I overlooked myself:
app.setOnRoomUserUpdateListener(new OnRoomUserUpdateListener() {
@Override
public void onUpdate(final User user, final int roomID, final int joinStatus) {
final String message;
if (joinStatus == OnRoomUserUpdateListener.USER_JOINED) {
message = context.getString(R.string.join_room_message, user.getUsername());
} else {
message = context.getString(R.string.leave_room_message, user.getUsername());
}
((Activity) context).runOnUiThread(new Runnable() {
@Override
public void run() {
messages.add(new Message(-1, user, message, System.currentTimeMillis(), System
.currentTimeMillis(), false, roomID, 0, true, Message.TYPE_JOINLEAVE));
messageAdapter.notifyDataSetChanged();
}
});
}
});
This code block is located below the code above and it's obviously also modifying the content of the adapter. When both of them run simultaneous, there could be a problem, right? Could this be fixed by using synchronized
or is there a better way?
EDIT 2: Initialization:
// get all messages
messages = new ArrayList<Message>();
messageDatabase.getConditionBuilder().add(DatabaseHelper.KEY_MESSAGE_ROOM_ID + " = ?",
new String[] { String.valueOf(roomID) });
messageDatabase.getConditionBuilder().setSortOrder(DatabaseHelper.KEY_MESSAGE_LOCAL_TIME + " DESC");
messageDatabase.getConditionBuilder().setSqlLimit(100);
messages.addAll(messageDatabase.getList());
// get "user joined/left" messages
UserDatabase userDatabase = UserDatabase.getInstance(context);
messages.addAll(userDatabase.getJoinLeaveMessages(roomID));
Collections.sort(messages);
messageAdapter = new MessageAdapter(getActivity(), R.layout.list_message_item, messages);
listView.setAdapter(messageAdapter);
Edit 3: Full source of the fragment, which contains the code: https://gist.github.com/ChristopherWalz/89a071b1606460e18ce7
First of all, let's take a look at the code in ListView
that throws this exception:
@Override
protected void layoutChildren() {
// ... code omitted...
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only from "
+ "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
// ... code omitted...
}
[you might notice that the message is different, but it is just because you're using old Android - the message has been made more informative in September `13]
Let's see where mItemCount
member variable is declared... Hmm, this variable seems to be inherited all the way from the AdapterView
class. Ok, let's find all the assignments in all the classes:
Basically, except for a single assignment to 0
(which is in onIvalidated()
method and can be ignored), this variable is always assigned to Adapter
's getCount()
value.
We can conclude that you get the exception because there is some concurrent code which alters your Adapter
's data while it is used to draw the contents of the ListView
.
Now, from your question it looks like you suspect that there is some kind of "congestion" of updates on the UI thread because there are too many messages... However, let's keep in mind that the code of Runnable.run()
which you post to UI thread for execution executes atomically - each Runnable
is popped from the UI thread's event queue and is run to completion before any other event gets a chance to be processed.
The above means that messages
will be updated with a new data and messageAdapter
will handle the change immediately, and no other event can interfere with this flow (as long as messages.add()
and messages.addAll()
in your code are synchronous calls). Bottom line: the code that dispatches Runnables
to the UI thread looks fine, and it is unlikely that it is the source of the problem. Furthermore, the stack trace of the exception does not contain any reference to Adapter
.
Up until now we summarized facts. Let's start the guesswork.
I think that the issue is not in the code you've posted. I guess you do one (or more) of the following actions, each of which can lead to the exception you got:
messages
is the same data structure used by messageAdapter
internally. It might be the case that you modify messages
in some other part of the code, which is not running on UI thread. In this case this modification can happen while the ListView
is refreshed on the UI thread and lead to the exception [it is generally bad practice to "leak" Adapter
's data structure outside of the Adapter
object].messageAdapter
in other parts of the code not running on UI thread.ListView
has not completed its initialization yet. I hardly believe that this is the case, but in order to be on the safe side, I'd suggest you ensure that you register your listeners in onResume()
and unregister them in onPause()
EDIT:
Based on the code of your Fragment
I can see two possible causes for the exception:
CustomResponseHandler
you pass to ServerUtil.post()
will be called from background threads, then the fact that you remove object from messages
in onFailure()
may be the case.onResume()
and unregister in onPause()
- this might not be important for button click listeners, but it causes memory leaks when you pass these listeners to Application
object. You have memory leaks in your code.If none of the above helps, post the code of MessageAdapter
and MessageDatabase
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With