Viking Hammer

Android ListView: Maintain your scroll position when you refresh

Refreshing an Android ListView is a pretty common thing — but the best way to do it isn’t immediately obvious. Here’s my progress through the different patterns.

The Obvious

I figured that when I’ve downloaded the new list of stuff I want to show, that I could just create a new adapter and stuff it into the list.

EventLogAdapter eventLogAdapter = new EventLogAdapter(mContext, events);
mEventListView.setAdapter(eventLogAdapter);

This works, in that the list gets updated. But it always scrolls all the way back up to the top. Sometimes that might be what you want, but most of the time you’ll want to maintain your scroll position.

The Naive

I tried getting pixel-level scroll position using getScrollY(), but it always returned 0. I don’t know what it’s supposed to do. I ended up going with a solution that got close to maintaining your scroll position.

int firstPosition = mEventListView.getFirstVisiblePosition();
EventLogAdapter eventLogAdapter = new EventLogAdapter(mContext, events);
mEventListView.setAdapter(eventLogAdapter);
mEventListView.setSelection(firstPosition);

This figures out the first item in the list you can see before resetting the adapter, and then scrolls you to it. This maintains your scroll position within some unknown/arbitrary range, and can cause you to jump around in the list a little bit when you refresh.

If you’re scrolled halfway through a list item, it’ll snap you to the top of it so it’s completely visible. Unfortunately, if you’re scrolled halfway through a list item, that probably wasn’t the one you were paying the closest attention to.

The Elegant

There had to be a better way!

And, of course, there is. Romain Guy, an Android developer who haunts Stack Overflow and Google Groups dropping golden hints when people ask questions, pointed out:

The problem is that you are creating a new adapter every time you reload the data. That’s not how you should use ListView and its adapter. Instead of setting a new adapter (which causes ListView to reset its state), simply update the content of the adapter already set on the ListView. And the selection/scroll position will be saved for you.

There are two problems with Romain Guy: 1) he doesn’t have a central repository of these hints/answers so I can learn what to do before doing everything wrong first, and 2) they really are just hints, in that they point you in the right direction without getting you all the way there.

In this case, yes, updating the adapter without creating a new one every time will maintain your scroll position. Except that you’ll frequently get an “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.”

It turns out that you need to call notifyDataSetChanged() on your adapter after changing its contents; fortunately, that “only from the UI thread” bit was a red herring, because I didn’t really want to do any processing that could/should be asynchronous on the UI thread.

I added a refill() method to my adapters:

public void refill(List<EventLog> events) {
    mEvents.clear();
    mEvents.addAll(events);
    notifyDataSetChanged();
}

And I call it when my download is complete:

if (mEventListView.getAdapter() == null) {
    EventLogAdapter eventLogAdapter = new EventLogAdapter(mContext, events);
    mEventListView.setAdapter(eventLogAdapter);
} else {
    ((EventLogAdapter)mEventListView.getAdapter()).refill(events);
}

If the list doesn’t already have an adapter, then I create one. But if it does have an adapter, then I just refill it. And it maintains my scroll position exactly!



4 Comments

  1. Nobi says:

    Hello there,

    i’m interesting with this entry,

    can you explain mEvents in refill method? is it same with mEventListView? and currently i have problem how to update listview with new item automatically added to the top of the listview. can you help me? :D

    Thank you,

    Regards,

    Nobi

  2. sirsean says:

    Sorry, yes. The refill method is on the EventLogAdapter class, and mEvents is an instance variable inside EventLogAdapter.

    The mEventListView variable belongs to my Activity, and we set its adapter to be the EventLogAdapter we create with a list of our events that we want to display.

    If you wanted to update a list with a new item at the top, you’d change the data such that there was a new item at index 0 (or get a new list with the new item first), and then call refill:

    ((EventLogAdapter)mEventListView.getAdapter()).refill(events);
    

    (In that example, the “events” variable is a list of objects that you want to display in the ListView, and can either be the same one you used to create the EventLogAdapter or an entirely new list. I usually use a new list.)

  3. peno says:

    Thanks for great article, works well…

  4. Lamar says:

    Thanks. Romain’s comment got me thinking. I do my main query in an AsyncTask that outputs a cursor. In the “protected void onPostExecute(Cursor mainCursor)” method, I include the following code. The changes worked well. My data gets refreshed and the list items do not move one single pixel.

    MainList.this.mAdapterCursor = mainCursor; MainList.this.startManagingCursor(MainList.this.mAdapterCursor);

    if (MainList.this.mCursorAdapter==null) {

    //Create the cursor adapter
    MainList.this.mCursorAdapter = new MainListCursorAdapter(
                        MainList.this, MainList.this.mAdapterCursor);
    
    //Set the list adapter to use the cursor adapter.
    MainList.this.setListAdapter(MainList.this.mCursorAdapter);
        MainList.this.setSelection(MainList.this.mFirstVisiblePosition);
    
    } else {
    
    //Refreshes list with new data set
    MainList.this.mCursorAdapter.changeCursor(MainList.this.mAdapterCursor);
    
    }
    

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>