Saturday, January 15, 2011

Android: Dynamically extending list views: part 1

(Part 1 of a two part post)......

Android is a platform I have become interested in over the last few months - me and a significant number of other developers I think!

During the course of an implementation recently, I needed to find a way to dynamically extend a list view when the list was scrolled/flung to it's logical end, showing a progress bar as the final list item when more items are being fetched. List views are a commonly employed construct in Android development, and I expected this to be a well covered topic in the Android forums, but what posts I did find were sometimes helpful and sometimes not. So this post describes a simple way of implementing such a requirement. There are other more general implementations in existence, such as the Commonsware EndlessAdapter, which I elected not to use to increase my knowledge of development in Android. The mechanism shown here is therefore rather low level and should be wrapped into an extensible component.

The key to a successful endeavour is to implement the interface android.widget.AbsListView.OnScrollListener (see here for the official documentation).  This defines a couple of methods that are called at various times in the life of a 'scrolling process' - onScroll(...) and onScrollStateChanged(...). The code that follows implements a simple mechanism for displaying a history for a bank account which is quite a reasonable example in this case - obviously, the transaction history magnitude can be significant (effectively unbounded) and we also need to be aware that cavalier consumption  of mobile bandwidth is not going to endear us to our users.


1:    public void onScroll(AbsListView view, int firstVisible,   
2:                    int visibleCount, int totalCount) {  
3:       if (mMoreTransactionsAvailable && !mBusy) {  
4:         // > 1 since we have a footer  
5:        final boolean loadMore = totalCount > 1 &&   
6:                             (firstVisible + visibleCount >= totalCount);  
7:        if (loadMore) {  
8:          mBusy = true;  
9:          new FetchTransactionHistoryTask().execute(  
10:                 new TransactionRequest(mAccountNumber,   
11:                                    ++mPageNumber,   
12:                                     getPageSize()));  
13:        }  
14:      }  
15:    }  
16:    public void onScrollStateChanged(AbsListView v, int s) {   
17:    }   

I didn't implement onScrollStateChanged, as I had no use for it. The onScroll is quite simple as you can see. In essence, it does the following:
  • Looks at a state variable to see if we believe more transactions are to be fetched, and we are not already in the process of doing so (that is, an asynchronous task is not already being executed)
  • Since onScroll gets called frequently, we then actually see if the progress bar that we display when the list is being auto extended is visible to the user - if so, we change our status to busy, and create and execute an asynchronous task with an object that represents the request that should be executed to retrieve the objects that should be appended to the list view
In onCreate(..) or similar, we have to add the current activity as a scroll listener, using code similar to this: getListView().setOnScrollListener(this);.

The asynchronous task is shown below, and is a private nested class of the list view implementation. It does very little in point of fact, as it communicates with a Facade object that wraps web service access, JSON parsing and so on. Once the facade has completed, the bind method is called of the enclosing list view implementation, which appends the new transaction objects to the existing list adapter.

1:     private class FetchTransactionHistoryTask extends   
2:            AsyncTask<TransactionRequest, Void, TransactionSet> {  
3:        protected void onPreExecute() {  
4:        }  
5:        protected TransactionSet doInBackground(final TransactionRequest... args) {  
6:           final TransactionRequest req = args[0];  
7:           return Facade.transactions(req.getAccountNumber(),   
8:                      req.getPageNumber(),   
9:                      req.getPageSize()).getPayload();  
10:        }  
11:        protected void onPostExecute(final TransactionSet set) {  
12:           bind(set);  
13:        }  
14:     }  

This is not a virtualized implementation, so performance issues will be apparent as the list grows - practically I have seen the usable upper bound to be between 400 and 600 objects (on Android 1.6 and 2.2). ListView itself is virtualized, in terms of View objects, but the adapter I implemented is not.

1:  private void bind(TransactionSet set) {   
2:        if (set != null) {  
3:           mMoreTransactionsAvailable = set.getMoreAvailable();  
4:           checkFooterStatus();  
5:           if (mListAdapter != null)   
6:              mListAdapter.appendSet(set);  
7:           else {  
8:              mListAdapter = new TransactionHistoryListAdapter(this, R.layout.list_item_transaction, set);  
9:               setListAdapter(mListAdapter);  
10:           }  
11:        }  
12:      mBusy = false;  
13:     }  
14:     private void checkFooterStatus() {   
15:        if (mMoreTransactionsAvailable && mProgressView == null)  
16:           addFooter();  
17:        else if (!mMoreTransactionsAvailable)  
18:           removeFooter();  
19:     }  
20:     private void addFooter() {   
21:        mProgressView = View.inflate(this, R.layout.footer_transaction_history, null);  
22:        getListView().addFooterView(mProgressView, null, false);  
23:     }  
24:     private void removeFooter() {  
25:        if (mProgressView != null)  
26:           getListView().removeFooterView(mProgressView);  
27:     }  

No comments: