July 31, 2014

Tutorial: Autogrowing ListView

listgrowIf you are building Android apps sooner or later you will use scrolling lists. And if you have more than 100 entries in the list it’s a good idea to load such list gradually. Best example of such list is “Inbox” in the Mail app: as you scroll to the bottom, the list will pause, indicate that something is loading and then more entries are added and you can continue to scroll until you hit the bottom again.

This tutorial will walk you through all steps of the process. Warning – the code is conceptual: I’m omitting required methods and using code snippets only to illustrate the mechanics of the process. It’s up to you as a developer to extend it to fit your application or design

The steps

  1. As user scrolls – detect the end of the list
  2. Display a progress notification
  3. Ask for update
  4. Receive update (asynchronously) and extend list

Now lets walk through the individual steps…

Detecting the end of the list

[java]
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.main);
// The list defined as field elswhere
this.view = (ListView) findViewById(R.id.searchResults);
this.view.setOnScrollListener(new OnScrollListener() {
private int priorFirst = -1;
@Override
public void onScroll(final AbsListView view, final int first, final int visible, final int total) {
// detect if last item is visible
if (visible < total && (first + visible == total)) {
// see if we have more results
if (first != priorFirst) {
priorFirst = first;
onLastListItemDisplayed(total, visible);
}
}
}
});
[/java]

  • As we scroll we check if the sum of 1st visible item index and number of visible items matches total number of items currently in the list.
  • Once we detect condition we need to keep in mind that scroll events are issued so rapidly that we actually can have duplicate onScroll calls for each unique combination of “first,” “visible” and “total.” To deal with this, we’ll define a “priorFirst” counter and only process the event if our “first” parameter does not match the “priorFirst”
  • If we detect that, we update “priorFirst” to match “first”, hence preventing further processing of event for this unique combination of parameters
  • onlastListItemDisplayed method is our private method which contains code that requests the update and is covered in the next section

Display progress indicator

To display progress indicator you need to have two views for your single item: one is regular view that displays the usual info (search result, etc.) and the second one that is hidden (“gone”) that contains the progress indicator. Here’s example of XML layout file defining such arrangement:

[xml]
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="fill_parent"
android:layout_width="fill_parent" android:orientation="vertical">
<LinearLayout android:layout_height="wrap_content" android:id="@+id/itemContent" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_weight="1">
<!– Your regular item info goes here –>
</LinearLayout>
<LinearLayout android:layout_height="wrap_content" android:layout_width="fill_parent" android:id="@+id/itemLoading"
android:padding="3px" android:gravity="center" android:visibility="gone">
<ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/trobber"
android:indeterminate="true"></ProgressBar>
</LinearLayout>
</LinearLayout>
[/xml]

The second LinearLayout section contains a ProgressBar element that is normally hidden (android:visibility=”gone”)

So in your code you need to detect last item and flip visibility of these two sections

[java]
protected void onLastListItemDisplayed(int total, int visible) {
if (total &amp;amp;lt; this.meta.totalResults) {
// find last item in the list
View item = view.getChildAt(visible – 1);
item.findViewById(R.id.itemContent).setVisibility(View.GONE);
item.findViewById(R.id.itemLoading).setVisibility(View.VISIBLE);
requestUpdate(false);
}
}
[/java]

  1. You get your last item based on “visible” counter (minus one)
  2. Find 2 children of the last item and flip visibility
  3. Now it’s time to request next portion of list (private requestUpdate)

The requestUpdate() method in my case contains an asynchronous call to the background  service. You don’t need to use service, instead you can use background thread – the key is, your code should be asynchronous: “call and forget”. Your Activity that hosts ListView at this point will display last item on the list with a spinning trobber in the middle.

Ask and you shall receive

No matter what you are using, you should anticipate normal outcome or failure otherwise your Activity will display the spinning wheel indefinitely.
So, in my case I provide the service with a callback object (IDL interface) that contains onSuccess() and onFailure() methods. You may opt (if you are not using services) to use android.os.Handler instead but again – the idea is you are receiving these callbacks asynchronously.

  • If your update fails – you can pop up “Toast” and stop the spinner to let your user know that getting new items failed to process.
  • If your update is successful and you are getting new set of items, it’s time to add these to the list. Simply loop through it and use ArrayAdapter#add method to add to the list. Here’s public method from the extended ArrayAdapter class that does just that
    [java]
    public void extend(List&amp;amp;lt;SearchItem&amp;amp;gt; appendix) {
    for (SearchItem item : appendix) {
    add(item);
    }
    }
    [/java]

    The key here is not to add items directly to the internal list of items but to use android.widget.ArrayAdapter#add() API call to do so

  • Do not recreate the ListView or its adapter – you need to use existing references or you will jump to the first item on the list
  • Be aware that if you exit your Activity while your update is still in the process you need to take care of terminating the update in progress or you will get unhandled exception when your callback method is called (especially if you are using service)
  • I found that I actually don’t need to “re-flip” the item to display the content and hide the trobber: when update is processed the settings from the XML layout are used and item is returned to the status quo automatically. However – this is something that may be specific to my particular situation and it’s possible that you need to do the flip again in your code.

Acknowledgment and credits

Special thanks to Mark Murphy – the @commonsguy for his answers to my related questions on Stackoverflow.com. I recommend you to look at Mark’s “endless” adapter code that cleared quite few things for me.