The CommonsBlog

CWAC EndlessAdapter Users: Upgrade, Please

If you have been using my EndlessAdapter, I strongly encourage you to upgrade to v1.2.1. There had been various reports of an exception in the field, but only today was I given a reproducible test case for it, and that bug should now be fixed.

If you are already on 1.x, there should be no code changes. Just update your Android library project, or grab a fresh JAR.

The error report was for:

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.

Lesson #1: Error messages may be misleading. In this case, the exception points out that this may be a threading problem. However, in my case, it turned out that this was not a threading problem, which led to a couple of lost hours of debugging.

Lesson #2: If you do something in an Adapter that changes what getCount() returns, you need to call notifyDataSetChanged() immediately.

In the case of EndlessAdapter, getCount() will return the getCount() from the Adapter that it wraps, or that value plus one. The latter case is when EndlessAdapter was told that there might be more data, and so the extra item is a “pending view” used to denote that there is data being loaded.

EndlessAdapter tracks whether or not there is data to be loaded in a keepOnAppending. I dutifully made this be an AtomicBoolean, so it is a thread-safe value. However, what I did not take into account was that toggling keepOnAppending between true and false would cause getCount() to return a different value. Eventually, notifyDataSetChanged() would be called, after which I was safe, but there was a window of time in between there where manipulating the list — such as tapping on a row — would trigger the IllegalStateException.

What that IllegalStateException really means is that the ListView thinks there should be N rows, but the Adapter is now claiming that there are M rows, with M != N. These values are synchronized via a call to notifyDataSetChanged(), but if getCount() changes before notifyDataSetChanged() is called, you have the window for potential problem.

My change is simply to call notifyDataSetChanged() any time I change the value of keepOnAppending, thereby triggering getCount() to change its return value.

In the field, you would see this problem when the EndlessAdapter was told that we have no more data (toggling keepOnAppending from true to false) but the user taps on a list item before we get a chance to append the last chunk of data, which would trigger a notifyDataSetChanged() call.

Many thanks to jbenf for reporting the way to reproduce the error, which allowed me to make this fix.

Want an expert opinion on your Android app architecture decisions? Perhaps Mark Murphy can help!