Android Advanced Cursors

by
Tags: , , ,
Category: ,

If you have been doing Android development for even a little while, you have probably used the provided SQLite functionality to store and retrieve data in a local database on the Android device.

If not, you can head over to Lars Vogel’s excellent tutorial website to see how it all works, or to get a refresher.

For many applications, the basic Cursor is all you need. But what about more advanced requirements? What follows is an overview of some of the more advanced cursor options.

CursorJoiner

If you need data from two or more database tables, the most straightforward thing to do is to use a SQL query that joins the two tables. However, sometimes you already have sources for two separate queries (such as a CursorLoader based on a ContentProvider), and you may wish to just re-use them, without creating a new query (without, e.g., modifying the ContentProvider).

This is where CursorJoiner comes in handy. Unlike the other options listed below, CursorJoiner doesn’t produce a new cursor. Instead, it provides an Iterable interface over the joined cursors.

If the only reason that you want to join the two tables is to produce a Cursor, say, for use by a CursorAdapter, then you should probably just stick with writing SQL that joins the two tables. However, if you just want to iterate the data once, then CursorJoiner can be very helpful.

One of the more interesting uses that I’ve seen for CursorJoiner is for determining what has changed when a LoaderManager.LoaderCallbacks<Cursor> implementation gets an updated Cursor in it’s onLoadFinished method.

You can use the CursorJoiner to join the ‘old’ cursor and the ‘new’ cursor on their primary key, and then compare them. As you iterate the CursorJoiner, you get a CursorJoiner.Result enum that indicates whether the current row exits in the left cursor, the right cursor, or both.

Here is some sample code that determines what has been added to or deleted from a ContentProvider as a CursorLoader driving a CursorAdapter is updated:

@Override
public void onLoadFinished(Loader<cursor> loader, Cursor updatedCursor) {
   if(adapter.getCursor() != null) {
      Cursor oldCursor = adapter.getCursor();
      CursorJoiner joiner =
         new CursorJoiner(oldCursor,
             new String[] {Thing.ID},
             updatedCursor,
             new String[] {Thing.ID}
      );
      for(CursorJoiner.Result joinerResult : joiner) {
         switch (joinerResult) {
            case RIGHT:
               int thingId = updatedCursor.getInt(
                  updatedCursor.getColumnIndex(Thing.ID)
               );
               String thingName = updatedCursor.getString(
                  updatedCursor.getColumnIndex(Thing.NAME)
               );
               Log.d(TAG, "new Thing added - id: " + thingId
                    + ", name: " + thingName);
            break;
            case LEFT:
               int thingId = updatedCursor.getInt(
                  oldCursor.getColumnIndex(Thing.ID)
               );
               String thingName = updatedCursor.getString(
                  oldCursor.getColumnIndex(Thing.NAME)
               );
               Log.d(TAG, "Thing deleted - id: "
                    + thingId + ", name: " + thingName);
            break;
            case BOTH:
               int thingId = updatedCursor.getInt(
                  oldCursor.getColumnIndex(Thing.ID)
               );
               String thingName = updatedCursor.getString(
                  oldCursor.getColumnIndex(Thing.NAME)
               );
               Log.d(TAG, "Thing unchanged - id: "
                    + thingId + ", name: " + thingName);
            break;
      }
   }
   updatedCursor.moveToFirst();
   adapter.swapCursor(updatedCursor);
}

MatrixCursor

MatrixCursor is useful if you have a collection of data that is not in the database, and you want to create a cursor for it.  You simply construct it with an array of column names, and optionally an initial capacity. Then, you can add one row at a time to it by passing either an array of objects or an Iterable to its addRow() method. Then use it like any other cursor:

String[] columnNames = {"Column1", "Column2"};
MatrixCursor matrixCursor = new MatrixCursor(columnNames);
matrixCursor.addRow(new String[]{"value1", "value2"});
adapter.swapCursor(matrixCursor);

MergeCursor

MergeCursor allows you to present two or more cursors as a single cursor. You could get almost the same effect by doing a sql UNION query. However, MergeCursor allows you to have different columns in the various cursors, and still merge them.

This is very useful if you are presenting a list of heterogeneous items. I’ve used it with Emil Sjölander’s StickyListHeaders library, using a different cursor for each ‘section’, all presented to the StickyListHeader implementation as a single cursor via a MergeCursor.

One thing to be aware of when presenting heterogeneous items through a MergeCursor – you’ll probably want to add a pseudo-column to each of the cursors to make it easy for your ListAdapter (or whatever will be using the cursor) to be able to determine which type of data it’s dealing with on each row. A seemingly little-known trick here is that you can include literal columns when creating a cursor using the ContentProvider’s query method (and similar methods in other database classes). Just include the literal in the projection parameter to the query:

String projection = {Thing.ID, Thing.NAME, "'TYPE_THING' as TYPE"};
Cursor c = qBuilder.query(mDb,
                projection,
                selection,
                selectionArgs,
                groupBy,
                having,
                sortOrder);

CursorWrapper

Finally, CursorWrapper is a Wrapper class for Cursor that delegates all of its calls to the actual cursor object. As the documentation stated, the primary use for this class is to extend a cursor while overriding only a subset of its methods.

One example for the use of CursorWrapper is when you already have a cursor, but only want to present the first few rows of the cursor to a CursorAdapter. Override the getCount() method to return a number smaller than the actual cursor’s count, and the adapter will only present that number of results. This is similar to what you would have gotten had you added a LIMIT clause to the sql that produced the cursor.

With this collection of advanced Cursor tricks, you should be able to accomplish whatever you want with Cursors and Adapters.