A pretty common UI design pattern for ListViews on Android is displaying a loading footer for the dynamic loading of a list. It’s a pattern used by several well known apps - Twitter, Gmail and Instagram to mention a few - to display data without having to click a button to paginate through information. Essentially, as a user scrolls down to the end of a list, which indicates the end of the current page, a loading indicator is displayed at the footer of the list to notify the user that the list is in the process of populating more data.
While playing around with this pattern, I was quite surprise how not-so-straightforward it was to implement this (then again, that’s programming for you). With that said, I’d like to share my implementation with you in case you’re trying to use this pattern in your application.
Trial 1
The intuitive way to go about this is to (1) simply attach an Adapter with the first page of items to a ListView, (2) attach an OnScrollListener to detect if the bottom of the list has been reached, if so, (3) add a loading footer view to the list while retrieving more data, and when the retrieval process is done, remove the loading footer and (4) update the adapter with the recently pulled data. Sounds pretty straightforward right? Turns out, it’s not. Here’s some code snippets of the above approach. (1) & (2)ListView list = (ListView) findViewById(R.id.listview); MyAdapter adapter = new MyAdapter(context, items); list.setAdapter(adapter); list.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // Do nothing } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // threshold being indicator if bottom of list is hit if (firstVisibleItem = threshold) { pullMoreData(); } } });(3) & (4)
private void pullMoreData() {
doNetworkRequest(page); // Perform request for next page
list.addFooterView(loadingFooter);
}
@Override
public void onNetworkRequestComplete(Request request) {
list.removeFooterView(loadingFooter);
adapter.addAll(request.getData());
adapter.notifyDataSetChanged();
}
This implementation, however, does not result in the intended action - the footer never gets displayed. A work-around I did for this leads me to…
Trial 2
With my second trial, I did this: (1) attached the footer first before the adapter, and when the bottom of the list has been reached and new data has been retrieved, (2) reattach the footer and create a new adapter with the old+new data which is then reattached to the list. Finally, to bring the user back to the scroll position, I (3) keep track of the first visible item on the scroll view and set the list selection to be this item. Immediately, a few things must be popping up in your head such as: that must be slow! it’s a hack! there has to be a cleaner way! etc. I can’t agree with you more. (1), (2) & (3)ListView list = (ListView) findViewById(R.id.listview); list.addFooterView(loadingFooter); MyAdapter adapter = new MyAdapter(context, items); list.setAdapter(adapter); list.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // Do nothing } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // member variable for restoring selection mSelection = firstVisibleItem; // threshold being indicator if bottom of list is hit if (firstVisibleItem = threshold) { pullMoreData(); } } }); @Override public void onNetworkRequestComplete(Request request) { list.removeFooterView(loadingFooter); list.addFooterView(loadingFooter); MyAdapter newAdapter = new MyAdapter(this); newAdapter.addOldData(adapter); newAdapter.addAll(request.getData()); list.setAdapter(newAdapter); adapter = newAdapter; // Set table to last selection list.setSelection(mSelection) }Albeit some performance issues and jerkiness because of (3), this implementation actually works. Can we do better than this?
Trial 3
The trick, it turns out, is to attach the footer view before setting the adapter, this way, any combination of adding and removing of the footer view/s just works. Why does it have to be in order!? If you have the answer, please leave a comment. I’d love to knowListView list = (ListView) findViewById(R.id.listview); list.addFooterView(loadingFooter); MyAdapter adapter = new MyAdapter(context, items); list.setAdapter(adapter); // this step is important to not display the footer view // right of the bat. list.removeFooterView(loadingFooter); list.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // Do nothing } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // threshold being indicator if bottom of list is hit if (firstVisibleItem = threshold) { pullMoreData(); } } });I’ll share more Android quirks as they come up, I hope this helped.