CursorLoader
The previous section discussed the low-level details of how to perform database operations in Android using SQLiteDatabase. However, it did not discuss the fact that databases on Android are stored on the file system, meaning that accessing a database from the main thread should be avoided in order to keep an app responsive for the user. Accessing a database from a non-UI thread typically involves some type of asynchronous mechanism, where a request for database access is made and the response to the request is delivered at some point in the future. Because views can be updated only from the UI thread, apps need to make calls to update views on the UI thread even though the results to a database query may be delivered on a different thread.
Android provides multiple tools for executing potentially long-running code off the UI thread while having results processed in the UI thread. One such tool is the loader framework. For accessing databases, there is a specialized component of the Loader called CursorLoader, which, in addition to managing a cursor’s lifecycle with regard to an activity lifecycle, also takes care of running queries in a background thread and presenting the results on the main thread, making it easy to update the display.
Creating a CursorLoader
There are multiple pieces to the CursorLoader API. A CursorLoader is a specialized member of Android’s loader framework specifically designed to handle cursors. In a typical implementation, a CursorLoader uses a ContentProvider to run a query against a database, then returns the cursor produced from the ContentProvider back to an activity or fragment.
In order to use a CursorLoader, an activity gets an instance of the LoaderManager. The LoaderManager manages all loaders for an activity or fragment, including a CursorLoader.
Once an activity or fragment has a reference to its LoaderManager, it tells the LoaderManager to initialize a loader by providing the LoaderManager with an object that implements the LoaderManager.LoaderCallbacks interface in the LoaderManager.initLoader() method. The LoaderManager.LoaderCallbacks interface contains the following methods:
Loader<T> onCreateLoader(int id, Bundle args)
void onLoadFinished(Loader<T>, T data)
void onLoaderReset(Loader<T> loader)
LoaderCallbacks.onCreate() is responsible for creating a new loader and returning it to the LoaderManager. To use a CursorLoader, LoaderCallbacks.onCreate()creates, initializes, and returns a CursorLoader object that contains the information necessary to run a query against a database (through a ContentProvider).
Listing 5.11 shows the implementation of the onCreateLoader() method returning a CursorLoader.
Listing 5.11 Implementing onCreateLoader()
@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { Loader<Cursor> loader = null; switch (id) { case LOADER_ID_PEOPLE: loader = new CursorLoader(this, PEOPLE_URI, new String[] {"first_name", "last_name", "id"}, null, null, "id ASC"); break; } return loader; }
In Listing 5.11, the onCreateLoader() method first checks the ID it was passed to know which loader it needs to create. It then instantiates a new CursorLoader object and returns it to the caller.
The constructor of CursorLoader can take parameters that allow the CursorLoader to run a query against a database. The CursorLoader constructor called in Listing 5.11 takes the following parameters:
Content context: Provides the application context needed by the loader
Uri uri: Defines the table against which to run the query
String[] projection: Specifies the SELECT clause for the query
String selection: Specifies the WHERE clause which may contain “?” as placeholders
String[] selectionArgs: Defines the substitution variables for the selection placeholders
String sortOrder: Defines the ORDER BY clause for the query
The last four parameters, projection, selection, selectionArgs, and sortOrder, are similar to parameters passed to the SQLiteDatabase.query() discussed earlier in this chapter. In fact, they also do the same thing: define what columns to include in the result set, define which rows to include in the result set, and define how the result set should be sorted.
Once the data is loaded, Loader.Callbacks.onLoadFinished() is called, allowing the callback object to use the data in the cursor. Listing 5.12 shows a call to onLoadFinished().
Listing 5.12 Implementing onLoadFinished()
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { while(data.moveToNext()) { int index; index = data.getColumnIndexOrThrow("first_name"); String firstName = data.getString(index); index = data.getColumnIndexOrThrow("last_name"); String lastName = data.getString(index); index = data.getColumnIndexOrThrow("id"); long id = data.getLong(index); //... do something with data }
Notice how similar the code in Listing 5.12 is to the code in Listing 5.10 where a direct call to SQLiteDatabase.query() was made. The code to process the results of the query is nearly identical. Also, when using the LoaderManager, the activity does not need to worry about calling Cursor.close() or making the database query on a non-UI thread. That is all handled by the loader framework.
There is one other important point to note about onLoadFinished(). It is not only called when the initial data is loaded; it is also called when changes to the data are detected by the Android database. There is one line of code that needs to be added to the ContentProvider to trigger this, and that is discussed next chapter. However, having a single point in the code that receives query data and can update the display can be really convenient. This architecture allows activities to easily react to changes in data without the developer worrying about explicitly notifying the activities of changes to the data. The LoaderManager handles the lifecycle and knows when to requery and pass the data to the LoaderManager.Callbacks when it needs to.
There is one more method in the LoaderManager.Callbacks interface that needs to be implemented to use a CursorLoader: LoaderManager.Callbacks.onLoaderReset(Loader<T> loader). This method is called by the LoaderManager when a loader that was previously created is reset and its data should no longer be used. For a CursorLoader, this typically means that any references to the cursor that was provided by onLoadFinished() need to be discarded as they are no longer active. If a reference to the cursor is not persisted, the onLoadReset() method can be empty.
Starting a CursorLoader
Now that the mechanics of using a CursorLoader have been discussed, it is time to focus on how to start a data load operation with the LoaderManager. For most use cases, an activity or a fragment implements the LoaderManager.Callbacks interface since it makes sense for the activity/fragment to process the cursor result in order to update its display. To start the load, LoaderManager.initLoader() is called. This ensures that the loader is created, calling onCreateLoader(), loading the data, and making a call to onLoadFinished().
Both activities and fragments can get their LoaderManager object by calling getLoaderManager(). They can then start the load process by calling LoaderManager.initLoader(). LoaderManager.initLoader() takes the following parameters:
int id: The ID of the loader. This is the same ID that is passed to onCreateLoader() and can be used to identify a loader (see Listing 5.11).
Bundle args: Extra data that might be needed to create the loader. This is also passed to onCreateLoader() (see Listing 5.11). This value can be null.
LoaderManager.LoaderCallbacks callbacks: An object to handle the LoaderManager callbacks. This is typically the activity or fragment that is making the call to initLoader().
The call to initLoader() should happen early in an Android component’s lifecycle. For activities, initLoader() is usually called in onCreate(). Fragments should call initLoader() in onActivityCreated() (calling initLoader() in a fragment before its activity is created can cause problems).
Once initLoader() is called, the LoaderManager checks to see if there is already a loader associated with the ID passed to initLoader(). If there is no loader associated with the ID, LoaderManager makes a call to onCreateLoader() to get the loader and associate it with the ID. If there is currently a loader associated with the ID, initLoader() continues to use the preexisting loader object. If the caller is in the started state, and there is already a loader associated with the ID, and the associated loader has already loaded its data, then a call to onLoadFinished() is made directly from initLoader(). This usually happens only if there is a configuration change.
One detail to note about initLoader() is that it cannot be used to alter the query that was used to create the CursorLoader that gets associated with an ID. Once the loader is created (remember, the query is used to define the CursorLoader), it is reused only on subsequent calls to initLoader(). If an activity/fragment needs to alter the query that was used to create a CursorLoader with a given ID, it needs to make a call to restartLoader().
Restarting a CursorLoader
Unlike the call to LoaderManager.initLoader(), a call to LoaderManager.restartLoader() disassociates a loader with a given ID and allows it to be re-created. This results in onCreateLoader() being called again, allowing a new CursorLoader object to be made which can contain a different query for a given ID. LoaderManager.restartLoader() takes the same parameter list as initLoader() (int id, Bundle, args, LoaderManager.Callbacks, and callbacks) and discards the old loader. This makes restartLoader() useful for when the query of a CursorLoader needs to change. However, the restartLoader() method should not be used to simply handle activity/fragment lifecycle changes as they are already handled by the LoaderManager.