The UI thread is a bad place for lengthy operations like loading data. You never know how long data will take to load, especially if that data is sourced from a content provider or the network. Android 3.0 (Honeycomb) introduced the concept of Loaders and, in particular, the CursorLoader class that offloads the work of loading data on a thread, and keeps the data persistent during short term activity refresh events, such as an orientation change. We'll incorporate the Loader as a new feature in our ongoing tutorial series building a yet-to-be-named tutorial reader application.
If you were paying close attention to our last tutorial, Android Fundamentals: Working With Content Providers, you may have noticed that we took a shortcut. We used the managedQuery() method of the Activity class, which is a newly deprecated method. This method represents the "old" way of letting an activity manage a cursor. Now we'll switch it to the new way, using a CursorLoader, as suggested in the latest SDK documentation. We can do this safely because although the CursorLoader class was included in Android 3.0, it is also part of the new compatibility library we discussed in Android Compatibility: Working with Fragments, and therefore can be used on devices as far back as Android 1.6.
Step 0: Getting Started
This tutorial assumes you will start where our tutorial called Android Fundamentals: Working With Content Providers left off. You can download that code and work from there, though you will have some tasks you'll have to do unassisted, or you can simply download the code for this tutorial and follow along. The choice is yours.
Step 1: Using the Right Class Versions
Normally, we can get away with just using the default import statement that Eclipse gives us. However, for loaders to work, we must ensure that we are using the correct versions of the classes. Here are the relevant import statements:
import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.SimpleCursorAdapter;
These imports replace the existing CursorAdapter import statements found in previous tutorial’s TutListFragmet class. This is the only class we'll need to modify. We don't normally talk about import statements, but when building this sample, Eclipse kept referencing the wrong CursorLoader package and we had to make the change manually, so we're calling it out here in our first step.
Step 2: Implementing Callbacks
Next, modify the TutListFragment class so that it now implements LoaderManager.LoaderCallbacks. The resulting TutListFragment class will now have three new methods to override:
public class TutListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> { // ... existing code // LoaderManager.LoaderCallbacks<Cursor> methods: @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { // TBD } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { // TBD } @Override public void onLoaderReset(Loader<Cursor> loader) { // TBD } }
Step 3: Initializing the Loader
You need to make several changes to the onCreate() method of the TutListFragment class. First, the Cursor will no longer be created here. Second, the loader must be initialized. And third, since the Cursor is no longer available immediately (as it will be loaded up in a separate thread instead), the initialization of the adapter must be modified. The changes to the onCreate() method are encapsulated here:
// TutListFragment class member variables private static final int TUTORIAL_LIST_LOADER = 0x01; private SimpleCursorAdapter adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] uiBindFrom = { TutListDatabase.COL_TITLE }; int[] uiBindTo = { R.id.title }; getLoaderManager().initLoader(TUTORIAL_LIST_LOADER, null, this); adapter = new SimpleCursorAdapter( getActivity().getApplicationContext(), R.layout.list_item, null, uiBindFrom, uiBindTo, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); setListAdapter(adapter); }
As you can see, we've made the three changes. The Cursor object and the resulting query() call have been removed. In it’s place, we call the initLoader() method of the LoaderManager class. Although this method returns the loader object, there is no need for us to keep it around. Instead, the LoaderManager takes care of the details for us. All loaders are uniquely identified so the system knows if one must be newly created or not. We use the TUTORIAL_LIST_LOADER constant to identify the single loader now in use. Finally, we changed the adapter to a class member variable and no cursor is passed to it yet by using a null value.
Step 4: Creating the Loader
The loader is not automatically created. That's a job for the LoaderManager.LoaderCallbacks class. The CursorLoader we'll need to create and return from the onCreateLoader() method takes similar parameters to the managedQuery() method we used previously. Here is the entire implementation of the onCreateLoader() method:
@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { String[] projection = { TutListDatabase.ID, TutListDatabase.COL_TITLE }; CursorLoader cursorLoader = new CursorLoader(getActivity(), TutListProvider.CONTENT_URI, projection, null, null, null); return cursorLoader; }
As you can see, it's fairly straightforward and really does look like the call to managedQuery(), but instead of a Cursor, we get a CursorLoader. And speaking of Cursors...
Step 5: Using the Cursor
You might be wondering what happened to the Cursor object? When the system finishes retrieving the Cursor, a call to the onLoadFinished() method takes place. Handling this callback method is quite simple:
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { adapter.swapCursor(cursor); }
The new swapCursor() method, introduced in API Level 11 and provided in the compatibility package, assigns the new Cursor but does not close the previous one. This allows the system to keep track of the Cursor and manage it for us, optimizing where appropriate.
Step 6: Implementing the Reset Callback
The final callback method to implement is the onLoaderReset() method. This method is triggered when the loader is being reset and the loader data is no longer available. Our only use is within the adapter, so we'll simply clear the Cursor we were using with another call to the swapCursor() method:
@Override public void onLoaderReset(Loader<Cursor> loader) { adapter.swapCursor(null); }
Step 7: Testing the Results
At this point, you're finished transitioning the TutListFragment to a loader-based implementation. When you run the application now, you'll probably notice that it basically behaves the same. So, how do you know this change is doing something? We leave this task as an exercise to the reader. How would you test the effectiveness of this solution?
HINT: It can be achieved with a single functional line of code that will require a try-catch block.
Conclusion
By moving the loading of data off the main UI thread of the application and into a CursorLoader, you've improved the responsiveness of the application, especially during orientation changes. If the content provider took 10 seconds per query, the user interface would not be negatively affected whereas in its previous implementation, it would have likely caused the dreaded "Force Close" dialog to appear.
About the Authors
Mobile developers Lauren Darcey and Shane Conder have coauthored several books on Android development: an in-depth programming book entitled Android Wireless Application Development and Sams Teach Yourself Android Application Development in 24 Hours. When not writing, they spend their time developing mobile software at their company and providing consulting services. They can be reached at via email to [email protected], via their blog at androidbook.blogspot.com, and on Twitter @androidwireless.
Comments