25

I use Cursors extensively in my app, to load and occasionally write information from and to a database. I have seen that Honeycomb and the Compatibility Package have new Loader classes designed to help with loading data in a "good" way.

Essentially, are these new classes (in particular CursorLoader) considerably better than previous methods of managing data? What is the benefit of a CursorLoader over managed Cursors for example?

And I use a ContentProvider to deal with data, which obviously takes Uris but how does this mesh with the initLoader() method? Must I set up each of my Fragments to use Loaders individually? And how unique does the id need to be for each loader, is it over the scope of my app or just a fragment? Is there any simple way of simply passing a Uri to a CursorLoader to query my data?

All I can see at the moment is that Loaders add an unnecessary extra step to getting my data into my app, so can someone explain them to me better?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Alex Curran
  • 8,818
  • 6
  • 49
  • 55

2 Answers2

44

There are two key benefits to using a CursorLoader in your app over Activity.managedQuery():

  1. The query is handled on a background thread for you (courtesy of being build on AsyncTaskLoader) so large data queries do not block the UI. This is something the docs recommended you do for yourself when using a plain Cursor, but now it's done under the hood.
  2. CursorLoader is auto-updating. In addition to performing the initial query, the CursorLoader registers a ContentObserver with the dataset you requested and calls forceLoad() on itself when the data set changes. This results in you getting async callbacks anytime the data changes in order to update the view.

Each Loader instance is also handled through the singular LoaderManager, so you still don't have to manage the cursor directly, and now the connection can persist even beyond a single Activity. LoaderManager.initLoader() and LoaderManager.restartLoader() allow you to reconnect with an existing Loader already set up for your query and, in some cases, instantly get the latest data if it is available.

Your Activity or Fragment will likely now implement the LoaderManager.Callback interface. Calling initLoader() will result in the onCreateLoader() method where you will construct the query and a new CursorLoader instance, if necessary. The onLoadFinished() method will be fired each time new data is available, and will include the latest Cursor for you to attach to the view or otherwise iterate through.

In addition, there is a pretty good example of all this fitting together on the LoaderManager class documentation page: http://developer.android.com/reference/android/app/LoaderManager.html

TylerH
  • 20,799
  • 66
  • 75
  • 101
devunwired
  • 62,780
  • 12
  • 127
  • 139
  • Cool, thanks for that explaination! Just a couple of questions: is the id supplied to `initLoader()` mean to be unique or each set of data you need to query? And because I have multiple activities which each use different tables of my database, is there any way I can create a class which handles each `CursorLoader`, much like a `ContentProvider` can handle multiple `Cursors` with `query`? – Alex Curran Aug 26 '11 at 14:21
  • 1
    The id value identifies a unique `Loader` instance with the manager, you can get access to the same one again if you ask `LoaderManager` sometime after it's already been created. Since each `CursorLoader` is basically created to execute a specific query, you could say that an id maps to a data set. Although there are methods on `CursorLoader` to dynamically change the query parameters as well, so that's not required to be the case. – devunwired Aug 26 '11 at 15:26
  • 2
    `LoaderManager` is really the single class that handles each `Loader` for you, but if you mean can you create a single class to manage `LoaderManager.Callback` for multiple `Loader` instances, the answer is yes. Multiple callbacks can be attached for a single `Loader` id value, and multiple ids could point back to the same callback implementation. – devunwired Aug 26 '11 at 15:26
10

If anyone finds themselves in a similar situation, here's what I've done:

  • Created a class which implements LoaderCallbacks and handles all the queries you'll need.
  • Supply this with a Context and the Adapter in question.
  • Create unique IDs for each query you'll use (if you use a UriMatcher, might as well use the same ones)
  • Make a convenience method which transfers queries into the bundle required for the LoaderCallbacks
  • That's pretty much it :) I put some of my code below to show exactly what I did

In my GlobalCallbacks class:

public static final String PROJECTION = "projection";
public static final String SELECTION = "select";
public static final String SELECTARGS = "sargs";
public static final String SORT = "sort";

Context mContext;
SimpleCursorAdapter mAdapter;

public GlobalCallbacks(Context context, SimpleCursorAdapter adapter) {
    mContext = context;
    mAdapter = adapter;
}

@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {

    Uri contentUri = AbsProvider.customIntMatch(id);
    if (contentUri != null) {
        return new CursorLoader(mContext, contentUri, args.getStringArray(PROJECTION), args.getString(SELECTION), 
                args.getStringArray(SELECTARGS), args.getString(SORT));
    } else return null;

}

@Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
    mAdapter.swapCursor(arg1);      
}

@Override
public void onLoaderReset(Loader<Cursor> arg0) {
    mAdapter.swapCursor(null);
}

And when I wanted to use a CursorLoader (Helper.bundleArgs() is the convenience bundling method):

scAdapt = new Adapters.NewIndexedAdapter(mHost, getMenuType(), 
                null, new String[] { "name" }, new int[] { android.R.id.text1 });
        getLoaderManager().initLoader(
                GlobalCallbacks.GROUP,
                Helper.bundleArgs(new String[] { "_id", "name" }),
                new GlobalCallbacks(mHost, scAdapt));
        setListAdapter(scAdapt);

And in Helper:

public static Bundle bundleArgs(String[] projection, String selection, String[] selectionArgs) {
    Bundle b = new Bundle();
    b.putStringArray(GlobalCallbacks.PROJECTION, projection);
    b.putString(GlobalCallbacks.SELECTION, selection);
    b.putStringArray(GlobalCallbacks.SELECTARGS, selectionArgs);
    return b;
}

Hope this helps someone else :)

EDIT

To explain more thoroughly:

  • First, an adapter with a null Cursor is initialised. We don't supply it with a Cursor because GlobalCallbacks will give the adapter the correct Cursor in onLoadFinished(..)
  • Next, we tell LoaderManager we want to initialise a new CursorLoader. We supply a new GlobalCallbacks instance (which implements Loader.Callbacks) which will then monitor the loading of the cursor. We have to supply it with the adapter too, so it can swap in the new Cursor once its done loading. At some point, the LoaderManager (which is built into the OS) will call onCreateLoader(..) of GlobalCallbacks and start asynchronously loading data
  • Helper.bundleArgs(..) just puts arguments for the query into a Bundle (e.g. columns projection, sort order, WHERE clause)
  • Then we set the Fragment's ListAdapter. The cursor will still be null at this point, so it will show a loading sign or empty message until onLoadFinished() is called
Alex Curran
  • 8,818
  • 6
  • 49
  • 55
  • Hey, I have a quick question. First though, I want to thank you for providing this. You really helped me understand what I needed to do and where to look. So my question is about your convenience method. I'm not really sure how it works. Could you explain that last bit of code if you have time please. Thanks again! – Andy Jun 17 '12 at 21:33
  • See my updated answer, let me know if you need anything further – Alex Curran Jun 17 '12 at 22:49
  • Ohhhhhh, I see. I finally got it!! I kind of understand. But How is that `Bundle` sent into `onCreateLoader`? The tutorial I saw does it right in onCreateLoader, never really using it. I was under the assumption that I didn't need to use it. – Andy Jun 17 '12 at 22:56
  • I think the Bundle is more useful if you want to dynamically change your query. For example if you had a list of items and you wanted all of them, you wouldn't need the Bundle, but it could come in handy if you want to then filter them, as you wouldn't need two different CursorLoaders (you'd just check if there was a filter query in the Bundle). – Alex Curran Jun 17 '12 at 23:01
  • Ahh, I really appreciate your time. One more thing, could you rewrite that convenience method exactly how you have it, and which class its in? But besides that, i'm all set. You have helped me out immensely. I can't thank you enough! – Andy Jun 17 '12 at 23:05
  • I'm afraid I don't have it any more (I was having issues with ProGuard and this class so I got rid of it), but I'll rewrite it above – Alex Curran Jun 17 '12 at 23:18
  • Oh, ok. I really appreciate it. – Andy Jun 17 '12 at 23:21
  • No probs, I've rewritten the base bundleArgs() above, you can just make the others passing null arguments. – Alex Curran Jun 17 '12 at 23:22