Monday, March 14, 2011

Cursor Adapters

If your Android app needs to present a large amount of data to your users, reading it from the database, being strict mode compliant, and maintaining a snappy GUI is a challenge. There are a lot of places where strict mode violations can creep in and produce undesirable side effects such as ANRs and a sluggish GUI. A good technique to help combat this problem is the use of Database Cursors and Android's CursorAdapter class.

Unfortunately, the pre-Honeycomb CursorAdapter class needs a little TLC before you can achieve strict mode compliance and a responsive GUI. The issue with the legacy implementation is that upon instantiation, it will execute the SQLite query. This is means, if you create your CursorAdapter on the Main ( or GUI ) thread, you are performing a database read on this thread. This is a big no-no, among other things, all DB reads and writes must be executed in a background thread.

But don't worry, it's not entirely your fault, this is how Google designed the API. The good news is you they've addressed it in Honeycomb and in their Honeycomb compatibility library. If you're interested in using this new API to construct CursorAdapters, checkout this link. Also, I will be doing a detailed blog post demonstrating how to use this API for pre-Honeycomb apps.

If you don't want to introduce yet-another-library into your Android project, here's a technique to create CursorAdatpers in the background. At a high-level you'll do the following

1.) In your Activity's onCreate( ) method, instantiate your CursorAdapter and pass in null for the Cursor argument

2.) In your Activity's onStart( ) method, kick off an AsyncTask that executes the desired DB query and returns a Cursor object

3.) In your AsyncTask's onPostExecute( ) method, call CursorAdapter.changeCursor(...) supplying the newly created Cursor.

4.) In your Activity's onDestroy( ) method, call CursorAdapter.changeCursor( null ). This will close the Cursor currently in use by the CursorAdapter.

I prefer this technique over using the Activity's built-in startManagingCursor( ) method because startManagingCursor( ) will automatically call Cursor.requery( ) for you. When it comes to UI threads and DB queries I prefer to be have control of what's going on. And to support my paranoia, Google is advising developers not to use Cursor.requery( ), which implies you shouldn't be using Activity.startManagerCursor( ) either. An official description can be found here.

Now, the code. Forgive the crudeness of the code, this is just a skeleton example used to provide an outline of the process. In the real-world, while your AsyncTask is running in the background, you will likely have a snazzy slash screen or animation entertaining your users while your app works diligently in the background. Enjoy.


public class MainActivity extends Activity { 
 /**
  * Flag used to discover if the activity has been destroyed while
  * CursorTask querying in the background
  */
 private boolean destroyed;
 
 /**
  * The CursorAdapter we'll be manipulating
  */
 private CursorAdapter cursorAdapter;
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   destroyed = false;
   cursorAdapter = new ExampleCursorAdapter( this );
   // other initialization goes here...
 }
    
 @Override
 protected void onStart() {
   super.onStart();
   new CursorTask().execute();
   // other destruction goes here...
 }
    
 @Override
 protected void onDestroy() {
   destroyed = true;
   super.onDestroy();
   // calling changeCursor() will close the previous cursor
   if( cursorAdapter != null ) { cursorAdapter.changeCursor( null ); }
 }
    
 /**
  * Called when the CursorTask returns from the background
  * @param cur
  */
 private void onCursorLoaded( Cursor cur ) {
   // This is the tricky part. Since the activity could've been
   // destroyed during the time CursorTask spent in the background
   // we have to make sure we haven't been destoyed and that
   // this.cursorAdapter is stil valid. We should be extra 
   // careful here because  we don't want to leak DB Cursors.
   if( !destroyed ) {
      cursorAdapter.changeCursor( cur );
   }
   else {
      cur.close();
   }
 }
    
 /**
  * Utility class used to load a cursor in the background and then
  * notify the foreground that it has a new Cursor 
  *
  */
 private class CursorTask extends AsyncTask  {
   /**
    * Run the query in the background so we don't cause ANRs
    */
  @Override
  protected Cursor doInBackground( Void... params ) {
  // do your database query here and return a cursor, intentionally
  // omitted
    return null;
  }

  /**
   * After doInBackground() returns, call onCursorLoaded() to
   * change cursorAdapter's Cursor
   */
  @Override
  protected void onPostExecute( Cursor result ) {
    onCursorLoaded( result );
  }
 }
    
 /**
  * A simple CursorAdapter implementation used to illustrate loading a Cursor in
  * the background 
  *
  */
 private static class ExampleCursorAdapter extends CursorAdapter {

 private static final int NAME_COL = 0;
     
   public ExampleCursorAdapter( Context context ) {
     super( context, null, false );
   }

   @Override
   public void bindView(View view, Context context, Cursor cursor) {
     // this is where you'll take the data from the cursor and
     // bind to view.  view is what is returned from the newView(...)
     // method
     ( ( TextView ) view ).setText( cursor.getString( NAME_COL ) );
   }

   @Override
   public View newView( Context context, Cursor cursor, ViewGroup parent ) {
     // here you'll create the View you want to be displayed, I typically
     // don't touch the cursor in this method because bindView(...) gets
     // called immediately after this.
     return new TextView( context );
   }
 }
}

2 comments:

Michal Vojtíšek said...

Hi, thank you for this nice example. Could you provide extended version to show how to handle subqueries with cache in custom cursor adapter?
This might be usefull when loading sms conversations and contact names.

tension said...

Hi Michal, if can you provide a concrete example of what you'd like to see, I'd be happy to draft an example.

-a