Going beyond CursorLoaders
19 Apr 2015CursorLoaders are a great way to load data from a content provider in the background while managing correctly the Activity lifecycle. When creating the CursorLoader in the onCreateLoader()
method, you specify a url, projections and selections with their arguments to be executed against the content provider and get result back. Once the query is executed in the background, a Cursor is returned to your activity via the onLoadFinished()
method. This is great but limited to one and only one query against the content provider.
Recently I needed to return the content of a directory from a content provider. I had to display the list of files as well as the parent folder (the directory itself).
In this article, I’m going to explain a simple way to go further with loaders by extending the AsyncTaskLoader class. As a result, we’ll be able to execute multiple queries as well as other long-lasting operations in the background.
Understanding
We are going to modify our activity to use our own implementation of the loader pattern. If you followed the tutorial on developer.android.com to query a content provider with CursorLoaders, your activity must be implementing the LoaderManager.LoaderCallbacks<D>
interface and override the three following methods:
public Loader<D> onCreateLoader(int id, Bundle args);
public void onLoadFinished(Loader<D> loader, D data);
public void onLoaderReset(Loader<D> loader);
Typically the interface Cursor is used for the generic part of the implementation (like this LoaderManager.LoaderCallbacks<Cursor>
) as the object returned by the Loader. Since we want to return more than one Cursor and perform more than one query operation in the background, we are going to use our own implementation of the Cursor interface thanks to the CursorWrapper class.
Setting up the activity
public class MainActivity implements LoaderManager.LoaderCallbacks<DirectoryCursor>
{
@Override
public Loader<DirectoryCursor> onCreateLoader (int loaderId, Bundle args)
{
return new DirectoryCursorLoader(this);
}
@Override
public void onLoadFinished (Loader<DirectoryCursor> loader, DirectoryCursor data)
{
if(data != null)
{
File directory = data.getDirectory(); // The directory item
mAdapter.changeCursor(data); // data is a Cursor containing the list of files inside that directory
}
}
@Override
public void onLoaderReset (Loader<DirectoryCursor> loader)
{
if (mAdapter != null)
mAdapter.changeCursor(null);
}
}
Now that the activity is set up, let’s implement the DirectoryCursor and DirectoryCursorLoader classes.
Using a different cursor
public class DirectoryCursor extends CursorWrapper
{
private final File directory;
public DirectoryCursor (File directory, Cursor childrenCursor)
{
super(childrenCursor);
this.directory = directory;
}
public File getDirectory()
{
return this.directory;
}
}
CursorWrapper is just a class implementing the Cursor interface. By extending it, we can override all the methods of the Cursor interface. In our case we don’t want to do any of that, we just want to add more data to the class. Inside the constructor, we are calling the super method with our cursor containing all the files belonging to the directory. This will insure a transparent implementation and will work exactly the same from a CursorAdapter standpoint.
Extending the AsyncTaskLoader
We are going to implement our DirectoryCursorLoader class which will load data in the background. This one is a little longer to implement. To achieve the following code, I’ve copied the source code of the CursorLoader class, removed the parts we didn’t need and then I’ve implemented the loadInBackground()
method which will be responsible of our custom logic. Let’s take a look at this class and the loadInBackgorund()
method:
public abstract class DirectoryCursorLoader extends AsyncTaskLoader<DirectoryCursor>
{
final ForceLoadContentObserver mObserver;
DirectoryCursor mCursor;
public DirectoryCursorLoader (Context context)
{
super(context);
mObserver = new ForceLoadContentObserver();
}
/* Runs on a worker thread */
@Override
public DirectoryCursor loadInBackground ()
{
DirectoryCursor directoryCursor = null;
Cursor parentCursor = null;
Cursor childrenCursor = null;
try
{
// Execute our first query against the content provider which will get the parent directory
parentCursor = getContext().getContentResolver().query([...]);
if (parentCursor != null && parentCursor.moveToFirst())
{
// Map the cursor to an object. This is basically getting all the columns from the cursor and setting them as instance variables inside our newly created File object.
File directory = new File(parentCursor);
// Execute our second query based on the result of the first one which will get the list of files belonging to the directory object
childrenCursor = getContext().getContentResolver().query([...]);
if (childrenCursor != null)
{
directoryCursor = new DirectoryCursor(directory, childrenCursor);
try
{
// Ensure the cursor window is filled.
directoryCursor.getCount();
directoryCursor.registerContentObserver(mObserver);
}
catch (RuntimeException ex)
{
directoryCursor.close();
throw ex;
}
}
}
}
finally
{
if(parentCursor != null)
parentCursor.close();
}
return directoryCursor;
}
@Override
protected void onStartLoading ()
{
if (mCursor != null)
{
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null)
{
forceLoad();
}
}
@Override
protected void onStopLoading ()
{
cancelLoad();
}
@Override
public void onCanceled (DirectoryCursor cursor)
{
if (cursor != null && !cursor.isClosed())
{
cursor.close();
}
}
@Override
protected void onReset ()
{
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed())
{
mCursor.close();
}
mCursor = null;
}
/* Runs on the UI thread */
@Override
public void deliverResult (DirectoryCursor cursor)
{
if (isReset())
{
// An async query came in while the loader is stopped
if (cursor != null)
{
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted())
{
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed())
{
oldCursor.close();
}
}
}
As you can see in the loadInBackground()
method, we’ve performed two different queries against the content provider. This is just an example of what you can do.
An other great use of this is creating sections in your listview. Instead of looping over all the entries in the cursor adapter to figure out which one are sections or to insert sections between different items in the UI Thread doing it in the loadInBackground()
method is way more efficient and will prevent jank.