This post gives a brief introduction to Loaders and the LoaderManager. The first section describes how data was loaded prior to the release of Android 3.0, pointing out out some of the flaws of the pre-HoneyComb APIs. The second section defines the purpose of each class and summarizes their powerful ability in asynchronously loading data.
This is the first of a series of posts I will be writing on Loaders and the LoaderManager:
- Part 1: Life Before Loaders
- Part 2: Understanding the LoaderManager
- Part 3: Implementing Loaders
- Part 4: Tutorial: AppListLoader
If you know nothing about Loaders and the LoaderManager, I strongly recommend you read the documentation before continuing forward.
The Not-So-Distant Past
Before Android 3.0, many Android applications lacked in responsiveness. UI interactions glitched, transitions between activities lagged, and ANR (Application Not Responding) dialogs rendered apps totally useless. This lack of responsiveness stemmed mostly from the fact that developers were performing queries on the UI thread--a very poor choice for lengthy operations like loading data.
While the documentation has always stressed the importance of instant feedback, the pre-HoneyComb APIs simply did not encourage this behavior. Before Loaders, cursors were primarily managed and queried for with two (now deprecated) Activity methods:
public void startManagingCursor(Cursor)Tells the activity to take care of managing the cursor's lifecycle based on the activity's lifecycle. The cursor will automatically be deactivated (
deactivate()) when the activity is stopped, and will automatically be closed (close()) when the activity is destroyed. When the activity is stopped and then later restarted, the Cursor is re-queried (requery()) for the most up-to-date data.public Cursor managedQuery(Uri, String, String, String, String)A wrapper around the
ContentResolver'squery()method. In addition to performing the query, it begins management of the cursor (that is,startManagingCursor(cursor)is called before it is returned).
While convenient, these methods were deeply flawed in that they performed queries on the UI thread. What's more, the "managed cursors" did not retain their data across Activity configuration changes. The need to requery() the cursor's data in these situations was unnecessary, inefficient, and made orientation changes clunky and sluggish as a result.
The Problem with "Managed Cursors"
Let's illustrate the problem with "managed cursors" through a simple code sample. Given below is a ListActivity that loads data using the pre-HoneyComb APIs. The activity makes a query to the ContentProvider and begins management of the returned cursor. The results are then bound to a SimpleCursorAdapter, and are displayed on the screen in a ListView. The code has been condensed for simplicity.
public class SampleListActivity extends ListActivity {
private static final String[] PROJECTION = new String[] {"_id", "text_column"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Performs a "managed query" to the ContentProvider. The Activity
// will handle closing and requerying the cursor.
//
// WARNING!! This query (and any subsequent re-queries) will be
// performed on the UI Thread!!
Cursor cursor = managedQuery(
CONTENT_URI, // The Uri constant in your ContentProvider class
PROJECTION, // The columns to return for each data row
null, // No where clause
null, // No where clause
null // No sort order
);
String[] dataColumns = { "text_column" };
int[] viewIDs = { R.id.text_view };
// Create the backing adapter for the ListView.
//
// WARNING!! While not readily obvious, using this constructor will
// tell the CursorAdapter to register a ContentObserver that will
// monitor the underlying data source. As part of the monitoring
// process, the ContentObserver will call requery() on the cursor
// each time the data is updated. Since Cursor#requery() is performed
// on the UI thread, this constructor should be avoided at all costs!
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
this, // The Activity context
R.layout.list_item, // Points to the XML for a list item
cursor, // Cursor that contains the data to display
dataColumns, // Bind the data in column "text_column"...
viewIDs // ...to the TextView with id "R.id.text_view"
);
// Sets the ListView's adapter to be the cursor adapter that was
// just created.
setListAdapter(adapter);
}
}
There are three problems with the code above. If you have understood this post so far, the first two shouldn't be difficult to spot:
managedQueryperforms a query on the main UI thread. This leads to unresponsive apps and should no longer be used.As seen in the
Activity.javasource code, the call tomanagedQuerybegins management of the returned cursor with a call tostartManagingCursor(cursor). Having the activity manage the cursor seems convenient at first, as we no longer need to worry about deactivating/closing the cursor ourselves. However, this signals the activity to callrequery()on the cursor each time the activity returns from a stopped state, and therefore puts the UI thread at risk. This cost significantly outweighs the convenience of having the activity deactivate/close the cursor for us.The
SimpleCursorAdapterconstructor (line 33) is deprecated and should not be used. The problem with this constructor is that it will have theSimpleCursorAdapterauto-requery its data when changes are made. More specifically, the CursorAdapter will register a ContentObserver that monitors the underlying data source for changes, callingrequery()on its bound cursor each time the data is modified. The standard constructor should be used instead (if you intend on loading the adapter's data with aCursorLoader, make sure you pass0as the last argument). Don't worry if you couldn't spot this one... it's a very subtle bug.
With the first Android tablet about to be released, something had to be done to encourage UI-friendly development. The larger, 7-10" HoneyComb tablets called for more complicated, interactive, multi-paned layouts. Further, the introduction of the Fragment meant that applications were about to become more dynamic and event-driven. A simple, single-threaded approach to loading data could no longer be encouraged. Thus, the Loader and the LoaderManager were born.
Android 3.0, Loaders, and the LoaderManager
Prior to HoneyComb, it was difficult to manage cursors, synchronize correctly with the UI thread, and ensure all queries occured on a background thread. Android 3.0 introduced the Loader and LoaderManager classes to help simplify the process. Both classes are available for use in the Android Support Library, which supports all Android platforms back to Android 1.6.
The new Loader API is a huge step forward, and significantly improves the user experience. Loaders ensure that all cursor operations are done asynchronously, thus eliminating the possibility of blocking the UI thread. Further, when managed by the LoaderManager, Loaders retain their existing cursor data across the activity instance (for example, when it is restarted due to a configuration change), thus saving the cursor from unnecessary, potentially expensive re-queries. As an added bonus, Loaders are intelligent enough to monitor the underlying data source for updates, re-querying automatically when the data is changed.
Conclusion
Since the introduction of Loaders in Honeycomb and Compatibility Library, Android applications have changed for the better. Making use of the now deprecated startManagingCursor and managedQuery methods are extremely discouraged; not only do they slow down your app, but they can potentially bring it to a screeching halt. Loaders, on the other hand, significantly speed up the user experience by offloading the work to a separate background thread.
In the next post (titled Understanding the LoaderManager), we will go more in-depth on how to fix these problems by completing the transition from "managed cursors" to making use of Loaders and the LoaderManager.
Don't forget to +1 this blog in the top right corner if you found this helpful! :)

Very nice. Not always when we see solutions we bother to ask ourselves what we are trying to solve in the first place, what we're trying to avoid.
ReplyDeleteYou touched a very important issue, which is the change in the programming paradigm, from linear to event-driven. My first published app lacked quite a lot (as many legacy apps there still do), and I've noticed that the single most important thing that I changed, to increase their quality, was my mind. Just applying this event mentality instantly gives you good results. Then you start applying multiprocessing better, and bingo! You are making applications like you never thought you could, with fast UIs and pleasant user experience.
Thanks for the reading.
How does one approach loaders in a dual-pane master/details scenario? At the moment my list view fragment has a loader and then when creating the details fragment to display it does an explicit query and passes that information to the details fragment via a newInstance methods that simply creates a new fragment and adds all the details to the argument list.
ReplyDeleteI've been thinking that maybe a better approach would be to use the loader like I do in the list view and then use a separate one in the details fragment and just pass the id from the list to the details and then let the loader logic kick in and populate the UI for me. Does this sound reasonable? Is there an "android way"?
What you described is how I would do it (pass an id to the details fragment instead of pre-queried data). I believe what you described is "the Android way" since it follows Fragment design guidelines. The details fragment would no longer rely on the list fragment to query its data for it, and would allow the details fragment to serve as an independent, reusable component throughout your entire application.
DeleteWhen it comes to fragments, the best option is usually the one that allows you to maximize the ease with which you can swap fragments in and out of your application. The previous case you described isn't as good because the details fragment depends on the list fragment in order to function properly.
I don't really agree. Loaders encourages lazyness in database design. You have no effort to make to have an application that stays responsive (in the sense of not having ANR), therefore you don't need to worry about data taking several seconds to actually load.
ReplyDeleteA well designed database loads data fast enough for it to be done on the UI Thread in most cases.
Thanks for the comment! I agree that developers shouldn't use Loaders as an excuse to design their databases poorly. That said, Loaders were introduced to rid Android apps of the ~0.1 seconds (or more) of lag that was occurring as a result of queries being performed directly on the UI thread. Even a perfectly designed database won't be able to perform queries instantaneously, so queries should still be offloaded to a separate thread.
DeleteExcellent
ReplyDelete