Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple push messages: The content of the adapter has changed but ListView did not receive a notification

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

like image 617
Chris Avatar asked Jul 02 '15 17:07

Chris


1 Answers

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:

enter image description here

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:

  1. I guess that 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].
  2. Similarly, you might be accidentally manipulating messageAdapter in other parts of the code not running on UI thread.
  3. It might be the case that you post events to UI thread while 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:

  1. If the methods of 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.
  2. As I suggested - register listeners in 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

like image 105
Vasiliy Avatar answered Oct 21 '22 23:10

Vasiliy