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.
Life before Loaders
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's query()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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
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:
- managedQuery performs a query on the main UI thread. This leads to unresponsive apps and should no longer be used.
- As seen in the Activity.java, the call to
managedQuerybegins management of the returned cursor with a call to
startManagingCursor(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 call
requery()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.
SimpleCursorAdapterconstructor (line 32) is deprecated and should not be used. The problem with this constructor is that it will have the
SimpleCursorAdapterauto-requery its data when changes are made. More specifically, the
CursorAdapterwill register a
ContentObserverthat monitors the underlying data source for changes, calling
requery()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 a CursorLoader, make sure you pass 0 as 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.
Prior to Honeycomb, it was difficult to manage cursors, synchronize correctly with the UI thread, and ensure all queries occurred 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.
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.
Understanding the LoaderManager
For now, you should think of Loaders as simple, self-contained objects that (1) load data on a separate thread, and (2) monitor the underlying data source for updates, re-querying when changes are detected. This is more than enough to get you through the contents of this post.
What is the LoaderManager?
Simply stated, the
LoaderManager is responsible for managing one or more Loaders associated with an
Fragment. Each Activity and each Fragment has exactly one LoaderManager instance that is in charge of starting, stopping, retaining, restarting, and destroying its Loaders. These events are sometimes initiated directly by the client, by calling
destroyLoader(). Just as often, however, these events are triggered by major Activity/Fragment lifecycle events. For example, when an Activity is destroyed, the Activity instructs its LoaderManager to destroy and close its Loaders (as well as any resources associated with them, such as a Cursor).
LoaderManager does not know how data is loaded, nor does it need to. Rather, the
LoaderManager instructs its
Loaders when to start/stop/reset their load, retaining their state across configuration changes and providing a simple interface for delivering results back to the client. In this way, the
LoaderManager is a much more intelligent and generic implementation of the now-deprecated
startManagingCursor method. While both manage data across the twists and turns of the Activity lifecycle, the
LoaderManager is far superior for several reasons:
startManagingCursormanages Cursors, whereas the
Loader<D>objects. The advantage here is that
Loader<D>is generic, where D is the container object that holds the loaded data. In other words, the data source doesn’t have to be a
Cursor; it could be a
JSONArray… anything. The
LoaderManageris independent of the container object that holds the data and is much more flexible as a result.
startManagingCursorwill make the
requery()on the managed cursor. As mentioned in the previous post,
requery()is a potentially expensive operation that is performed on the main UI thread. Subclasses of the
Loader<D>class, on the other hand, are expected to load their data asynchronously, so using the
LoaderManagerwill never block the UI thread.
startManagingCursordoes not retain the Cursor’s state across configuration changes. Instead, each time the Activity is destroyed due to a configuration change (a simple orientation change, for example), the
Cursoris destroyed and must be requeried. The
LoaderManageris much more intelligent in that it retains its Loaders’ state across configuration changes, and thus doesn’t need to requery its data.
LoaderManagerprovides seamless monitoring of data! Whenever the Loader’s data source is modified, the
LoaderManagerwill receive a new asynchronous load from the corresponding Loader, and will return the updated data to the client. (Note: the LoaderManager will only be notified of these changes if the Loader is implemented correctly. ).
If you feel overwhelmed by the details above, I wouldn’t stress over it. The most important thing to take away from this is that the
LoaderManager makes your life easy. It initializes, manages, and destroys Loaders for you, reducing both coding complexity and subtle lifecycle-related bugs in your Activitys and Fragments. Further, interacting with the
LoaderManager involves implementing three simple callback methods. We discuss the
LoaderManager.LoaderCallbacks<D> in the next section.
Implementing the LoaderManager.LoaderCallbacks
LoaderManager.LoaderCallbacks<D> interface is a simple contract that the
LoaderManager uses to report data back to the client. Each
Loader gets its own callback object that the
LoaderManager will interact with. This callback object fills in the gaps of the abstract
LoaderManager implementation, telling it how to instantiate the
Loader (onCreateLoader) and providing instructions when its load is complete/reset (
onLoadReset, respectively). Most often you will implement the callbacks as part of the component itself, by having your Activity or Fragment implement the
1 2 3 4 5 6 7 8 9 10
Once instantiated, the client passes the callbacks object (“this”, in this case) as the third argument to the
initLoader method, and will be bound to the
Loader as soon as it is created.
Overall, implementing the callbacks is straightforward. Each callback method serves a specific purpose that makes interacting with the
onCreateLoaderis a factory method that simply returns a new
LoaderManagerwill call this method when it first creates the
onLoadFinishedis called automatically when a
Loaderhas finished its load. This method is typically where the client will update the application’s UI with the loaded data. The client may (and should) assume that new data will be returned to this method each time new data is made available. Remember that it is the Loader’s job to monitor the data source and to perform the actual asynchronous loads. The
LoaderManagerwill receive these loads once they have completed, and then pass the result to the callback object’s
onLoadFinishedmethod for the client (i.e. the Activity/Fragment) to use.
onLoadResetis called when the Loader’s data is about to be reset. This method gives you the opportunity to remove any references to old data that may no longer be available.
Transitioning from Managed Cursors to the LoaderManager
The code below is similar in behavior to the sample in my previous section. The difference, of course, is that it has been updated to use the
CursorLoader ensures that all queries are performed asynchronously, thus guaranteeing that we won’t block the UI thread. Further, the
LoaderManager manages the
CursorLoader across the
Activity lifecycle, retaining its data on configuration changes and directing each new data load to the callback’s
onLoadFinished method, where the Activity is finally free to make use of the queried
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
As its name suggests, the
LoaderManager is responsible for managing Loaders across the Activity/Fragment lifecycle. The
LoaderManager is simple and its implementation usually requires very little code.
This section introduces the
Loader<D> class as well as custom
Loaders are responsible for performing queries on a separate thread, monitoring the data source for changes, and delivering new results to a registered listener (usually the
LoaderManager) when changes are detected. These characteristics make
Loaders a powerful addition to the Android SDK for several reasons:
They encapsulate the actual loading of data. The
Activity/Fragmentno longer needs to know how to load data. Instead, the
Activity/Fragmentdelegates the task to the
Loader, which carries out the request behind the scenes and has its results delivered back to the
They abstract out the idea of threads from the client. The Activity/Fragment does not need to worry about offloading queries to a separate thread, as the
Loaderwill do this automatically. This reduces code complexity and eliminates potential thread-related bugs.
They are entirely event-driven.
Loadersmonitor the underlying data source and automatically perform new loads for up-to-date results when changes are detected. This makes working with Loaders easy, as the client can simply trust that the Loader will auto-update its data on its own. All the Activity/Fragment has to do is initialize the
Loaderand respond to any results that might be delivered. Everything in between is done by the Loader.
Loaders are a somewhat advanced topic and may take some time getting used to. We begin by analyzing its four defining characteristics in the next section.
What Makes Up a Loader?
There are four characteristics which ultimately determine a Loader’s behavior:
A task to perform the asynchronous load. To ensure that loads are done on a separate thread, subclasses should extend
AsyncTaskLoader<D>as opposed to the
AsyncTaskLoader<D>is an abstract Loader which provides an
AsyncTaskto do its work. When subclassed, implementing the asynchronous task is as simple as implementing the abstract
loadInBackground()method, which is called on a worker thread to perform the data load.
A registered listener to receive the Loader’s results when it completes a load. For each of its
OnLoadCompleteListener<D>which will forward the Loader’s delivered results to the client with a call to
onLoadFinished(Loader<D> loader, D result). Loaders should deliver results to these registered listeners with a call to
Loader#deliverResult(D result).You don’t need to worry about registering a listener for your Loader unless you plan on using it without the
LoaderManagerwill act as this “listener” and will forward any results that the Loader delivers to the
One of three distinct states. Any given Loader will either be in a started, stopped, or reset state:
Loadersin a started state execute loads and may deliver their results to the listener at any time. Started Loaders should monitor for changes and perform new loads when changes are detected. Once started, the Loader will remain in a started state until it is either stopped or reset. This is the only state in which
onLoadFinishedwill ever be called.
Loadersin a stopped state continue to monitor for changes but should not deliver results to the client. From a stopped state, the
Loadermay either be started or reset.
Loadersin a reset state should not execute new loads, should not deliver new results, and should not monitor for changes. When a loader enters a reset state, it should invalidate and free any data associated with it for garbage collection (likewise, the client should make sure they remove any references to this data, since it will no longer be available). More often than not, reset Loaders will never be called again; however, in some cases they may be started, so they should be able to start running properly again if necessary.
- An observer to receive notifications when the data source has changed.
Loadersshould implement an observer of some sort (i.e. a
BroadcastReceiver, etc.) to monitor the underlying data source for changes. When a change is detected, the observer should call
Loader#onContentChanged(), which will either (a) force a new load if the Loader is in a started state or, (b) raise a flag indicating that a change has been made so that if the
Loaderis ever started again, it will know that it should reload its data.
By now you should have a basic understanding of how Loaders work. If not, I suggest you let it sink in for a bit and come back later to read through once more (reading the documentation never hurts either!). That being said, let’s get our hands dirty with the actual code!
Implementing the Loader
As I stated earlier, there is a lot that you must keep in mind when implementing your own custom Loaders. Subclasses must implement
loadInBackground() and should override
deliverResult(D results) to achieve a fully functioning
Loader. Overriding these methods is very important as the
LoaderManager will call them regularly depending on the state of the Activity/Fragment lifecycle. For example, when an Activity is first started, the Activity instructs the
LoaderManager to start each of its
Activity#onStart(). If a
Loader is not already started, the
startLoading(), which puts the
Loader in a started state and immediately calls the
onStartLoading() method. In other words, a lot of work that the
LoaderManager does behind the scenes relies on the Loader being correctly implemented, so don’t take the task of implementing these methods lightly!
The code below serves as a template of what a
Loader implementation typically looks like. The
SampleLoader queries a list of
SampleItem objects and delivers a
List<SampleItem> to the client:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146