IllegalStateException says to call 'notifyDataSetChanged() when its content changes'

261 Views Asked by At

I have seen many other questions on this topic/error, but none seem to be in my situation. The below LogCat related to my Nav Drawer list adapter.

(brief code below the LogCat)

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. Make sure your adapter calls notifyDataSetChanged() when its content changes. [in ListView(2131558613, class android.widget.ListView) with Adapter(class com.---.---.DrawerAdapter)]
    at android.widget.ListView.layoutChildren(ListView.java:1572)
    at android.widget.AbsListView.onTouchUp(AbsListView.java:3959)
    at android.widget.AbsListView.onTouchEvent(AbsListView.java:3758)
    at android.view.View.dispatchTouchEvent(View.java:9349)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2240)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2559)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2260)
    at com.android.internal.policy.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2453)
    at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1755)
    at android.app.Activity.dispatchTouchEvent(Activity.java:2776)
    at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:67)
    at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:67)
    at com.android.internal.policy.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2402)
    at android.view.View.dispatchPointerEvent(View.java:9590)
    at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4436)
    at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4292)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3816)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3875)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3841)
    at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3971)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3849)
    at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4028)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3821)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3875)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3841)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3849)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3821)
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6150)
    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6118)
    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6072)
    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6253)
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:216)
    at android.os.MessageQueue.nativePollOnce(Native Method)
    at android.os.MessageQueue.next(MessageQueue.java:323)
    at android.os.Looper.loop(Looper.java:144)
    at android.app.ActivityThread.main(ActivityThread.java:5845)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:797)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:687)

Inside my onCreate() in my MainActivity I simply do this:

        mDrawerList = (ListView) findViewById(R.id.drawer_list);
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        String[] calcTypeList = new String[]{"item1", "item2", "etc".}; // 13 items total
        // override with new list if user upgraded? (1 less item)
        if (myApp.didUpgrade) {
              calcTypeList = new String[]{"item1", "item2", "etc".}; // 12 items total
        }

        dAdapter = new DrawerAdapter(MainFragmentActivity.this, lcTypeList);
        mDrawerList.setAdapter(dAdapter);

At no point do I add anything to this list. The only time does it change is if a user "upgrades" the app, and then I remove an item.

This is the code that does that:

IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener
        = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result,
                                      Purchase purchase) {
        if (result.isFailure()) {
           Log.i("Purchase Result", "There was a problem with the purchase: " + result.getMessage());
            return;
        } else if (purchase.getSku().equals(SKU_UPGRADE)) {
            Toast.makeText(MainFragmentActivity.this, "Upgrade Successful!", Toast.LENGTH_SHORT).show();
            MyApp.didUpgrade = true;
            dAdapter.notifyDataSetChanged();
        }
    }
};

I think I have been reading that you are not suppose to add items or do notifyDataSetChanged() in a background thread. Not sure if that is a background thread above? Even so, I do not believe this is being triggered when the error happens. (But I won't totally discount it).

If the user does upgrade, then when the app starts -- since the upgrade menu item is the last item and i no longer need to show it, I simply do this:

in DrawerAdapter:

@Override
public int getCount() {
   if (MyApp.didUpgrade) {
            return 12;
   } else {
            return 13;
   }
}

I can't think of any other code.

All feedback is appreciated!

EDIT:

Updated code based on recommendation. The one instance of notifyDataSetChanged() is now being moved to a UI thread. However, this code is only called after the user directly clicks the in-app purchase and upgrades. I don't believe it is called again. The user with this error is reporting everytime they enter the app and click on a menu item.

IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener
            = new IabHelper.OnIabPurchaseFinishedListener() {
        public void onIabPurchaseFinished(IabResult result,
                                          Purchase purchase) {
            if (result.isFailure()) {
               Log.i("Purchase Result", "There was a problem with the purchase: " + result.getMessage());
                return;
            } else if (purchase.getSku().equals(SKU_UPGRADE)) {
                Toast.makeText(MainFragmentActivity.this, "Upgrade Successful!", Toast.LENGTH_SHORT).show();
                MyApp.didUpgrade = true;
                setDrawerAdapter();
            }
        }
    };

private void setDrawerAdapter() {

        new Thread() {
            public void run() {
                try {
                    runOnUiThread(new Runnable() {

                        @Override
                        public void run() {
                            dAdapter.notifyDataSetChanged();
                        }
                    });
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

UPDATE #2

The user reported this bug after a recent update, I had did. The app had not been updated for 9 months. The only changes I really did was chagne the targetSdkVersion from 23 -> 25. Also upgrade the depenencies. I do not recall the old versions, the news one are

    compile "com.android.support:appcompat-v7:25.0.1"
    compile 'com.android.support:cardview-v7:25.0.1'
    compile 'com.android.support:design:25.0.1'

NO code in my Nav drawer or adapter was otherwise changed int his update.

2

There are 2 best solutions below

1
On BEST ANSWER

I suggest you check for any other places where you write to MyApp.didUpgrade.
Perhaps in a callback from mHelper.queryInventoryAsync(..)?

I suspect the following:
1. You call mHelper.queryInventoryAsync to check the users inventory. The call is async, therefore the thread will wait for the answer.
2. The activity and its drawer content is inflated, the list is populated with 13 items.
3. queryInventoryAsync now returns and triggers your callback, you set MyApp.didUpgrade = true but forget to call notifyDataSetChanged().
4. For any reason the drawer content is redrawn - an exception is thrown because count() now returns 12 and android believes the list is corrupt.

Do you recieve error reports only from users who have already bought the upgrade?
Note also that the view in the drawer is inflated as soon as the activty is created, even if the drawer is not opened.

7
On

I searched a bit about this problem:

ListView caches the element count and whenever it has to layout its children, it'll check this count againt the current number of items in the bound adapter. If the count has changed and the ListView wasn't notified about this, an error is thrown:

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() + ")]");
}

Im not sure if onIabPurchaseFinished() is called on another thread. But if you really don't change anything on other places i think there's the problem. Let's say, it's on another thread. So when you call MyApp.didUpgrade = true; the adapters content is changed by another thread because you call if (MyApp.didUpgrade) { inside your adapter. Then the listview call layoutChildren() and has the old number of getcount(). I think this is why you get this error.

Try to call both lines in runOnUiThread()

MyApp.didUpgrade = true;
dAdapter.notifyDataSetChanged();