Android Loader
s, first introduced in Honeycomb (API 11), enable app devs to use a more robust pattern for handling data loading. Data loading is (generally) easy, so why was this needed? To answer that, we first have to take a step back and take a glimpse at an app’s internals.
Android App Context of Execution
It’s very common in Android app development to see references to the “UI thread”. This is the main thread of an application and how the framework notifies the app of events. Take an Activity
for example: its lifecycle callbacks as well as UI event callbacks (onTouch()
, onClick()
, etc.) are all executed on the main thread. Why is this relevant to a data discussion? Two reasons:
- While executing a lifecycle or UI callback, all other similar events for your app are waiting to be processed
- The current state of your
Activity
(orFragment
can change and has to be coordinated with background threads
Let’s start with the first item. If the Activity
is in the running state (e.g. onResume()
has run) and a click listener’s onClick()
is called, any additional touch, click or lifecycle changes are queued for processing after the listener returns. If the onClick()
takes a long time to complete, the other events are sitting in the queue waiting to be processed. If we have even a moderately sized app there’s a chance we also have a BroadcastReceiver
or Service
which could have events pending as well. These components are also called on the main thread of our app’s process. It should be pretty obvious how long running operations on the main thread can cause problems. Even small data loads, such as from an internal SQLite database, can take time due to I/O or processing. So, the bottom line is our data loading really needs to be handled off the main thread.
But, now that we have introduced a secondary thread (or more!) to the mix, we have to worry about lifecycle of the owning component. What happens if our thread completes its work but the Activity
is no longer in the running state? There’s potentially a big problem here. Therefore, resolving the first item above leads us into the second item. Funny how that works out!
Introducing the Forklift: the Android Loader
Anyone who has been working with Android prior to API 11 can tell you war stories around managing background threads and maintaining state across configuration changes (e.g. screen rotation). While these things are not impossible to deal with, they do require some careful considerations which are prone to problems. Along with the introduction of Fragments
in API 11, the Android Loader
APIs were also added to the framework. This helps make it easier to solve the problem of background data handling and coordinating with lifecycle changes. How does it do this? The Loader
mechanism is integrated with the Activity
and Fragment
classes of the framework. For those targeting pre-Honeycomb devices: don’t fear, you can still leverage the Android Loader
mechanism (and Fragment
s!) by way of the compatibility library. More on that later.
Back to how it works. Within the Activity
and Fragment
classes there is a LoaderManager
which maintains the instance of each Loader
and is aware of the lifecycle of the owning component. The manager is used to create, destroy and restart the Loader
when requested by the app code. Once the Loader
has been created, the manager will control the Loader
execution based on state. The manager also handles providing the results to the app when the Loader
has returned data. The Loader
itself is responsible for managing the data it retrieves and where/how it is pulled. Loader
s are intended to be asynchronous in nature, doing the loading/work on another thread, but the coordination here isn’t exactly clear from the documentation. This diagram shows a high level view of what typical Loader
operation looks like.
As you can see, when the Loader
is initialized via the call to LoaderManager.initLoader()
, the Loader
is created and once the Activity
or Fragment
goes through its ‘starting’ state the Loader
is told to start loading. It is at this point the Loader
can start its background loading. Think of it like a forklift (and driver) which has been given instructions on what/where to load, goes off to fetch the load, and will come banging on its supervisor’s door when it is ready to drop it off.
The Loader
class itself is abstract, it is up to the implementation to manage the data loading. There is a concrete implementation which can be used for databases or ContentProvider
s called CursorLoader
. This class is derived from AsyncTaskLoader
, which is an abstract class providing most of the basic plumbing for background operations. As you may have already guessed from the name, it uses AsyncTask
for its background thread handling. For those wishing to create their own Loader
classes, the AsyncTaskLoader
is what you will most likely extend for your implementation. I’m not going to cover custom Loader
implementations in this article, that’s a deeper topic for another day. Once the background loading has been completed, the Loader
provides the results via its deliverResult()
method. This method is always called on the main thread of the app, so it is up to the Loader
implementation to call this method in the correct context of execution.
Using a Loader
Now that we have covered the background concepts of the Loader
, and even some of its implementation details, let’s actually use one to get a better feel for it. The simple example I’m going to walk through is an app which shows the call log for the incoming calls on a phone. Something I’ve not discussed yet is how the LoaderManager
actually creates the Loader
instance after initLoader()
is called. It is up to our app code to do this, and it is given an opportunity to do so at the correct time by registering callbacks with the LoaderManager
during this init call. The callback mechanism is how our app code is informed which Loader
is desired as well as when it is done loading and if it needs to be restarted. Here in our Activity
‘s onCreate()
method, after inflating our layout and creating an adapter for our ListView
, we init an instance of the loader, specified by an ID (which is meaningful for our app):
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mInCallList = (ListView)findViewById(R.id.list_in_calls); mAdapter = new SimpleCursorAdapter(this, R.layout.list_recent_in_call, null, IN_CALL_COLUMNS, IN_CALL_RES_IDS, 0); mAdapter.setViewBinder(MY_VIEW_BINDER); mInCallList.setAdapter(mAdapter); Log.d(TAG, "onResume: initializing loader..."); getLoaderManager().initLoader(LOADER_MY_ID, null, this); }
Notice the last argument of the initLoader()
call, this. Our Activity
is implementing the LoaderManager.LoaderCallbacks
interface:
public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks {
Our implementation is shown below. The onCreateLoader()
method is where we create a Loader
which makes sense for our application. We are going to pull call logs from the CallLog
provider, so we’ll use a CursorLoader
, as shown in the code. The CursorLoader
returns a Cursor
object holding any data it pulled from the provider which matched the projection
and selection
arguments. In our case, we did not specify a projection
, so we get all columns. We did, however, specify a selection
: we only want “incoming” call types.
@Override public Loader onCreateLoader(int id, Bundle args) { Loader ret = null; String sel; String[] selArgs; Log.d(TAG, "onCreateLoader for id: " + Integer.toString(id)); switch (id) { case LOADER_MY_ID: sel = CallLog.Calls.TYPE + " = ?"; selArgs = new String[] { Integer.toString(CallLog.Calls.INCOMING_TYPE) }; ret = new CursorLoader(this, CallLog.Calls.CONTENT_URI, null, sel, selArgs, CallLog.Calls.DEFAULT_SORT_ORDER); break; default: Log.w(TAG, "Unhandled id: " + Integer.toString(id)); break; } return ret; }
Once this callback returns, the LoaderManager
for our Activity
has an instance of the Loader
and will tell it to load data at the appropriate time. Since we’re using a built-in Loader
, we won’t get to directly see when it is operating in another thread. However, once the data loading is complete, our Activity
is notified via another callback method: onLoadFinished()
. It is at this point we can take the new data and do something with it. In our case, all we need to do is take the Cursor
and swap it into our Adapter
. The Adapter
and ListView
will do the rest and ensure the UI is up to date with this latest data.
@Override public void onLoadFinished(Loader loader, Object data) { Log.d(TAG, "onLoadFinished for id: " + Integer.toString(loader.getId())); // We only have the single Loader, so no need to verify ID mAdapter.swapCursor((Cursor)data); }
Something else worth mentioning here is that the Loader
is automatically listening to the data source for updates. In our sample code, if a new call arrives and is handled so it makes it into the call log, we don’t have to do anything special to have our UI updated. The Loader
is already setting an observer on the data source, so when the data changes it pulls the new data and will call our onLoadFinished()
again. Notice that we did not close the previous Cursor
object. The Loader
is automatically releasing the backing data for us as well. Talk about convenient!
Gotchas
What would a framework API be without some gotchas? Especially ones which deal with multiple contexts of execution! While the Loader
handles our data and background data handling, they still must be handled with care. Here is a brief list of potential pain points, in no particular order:
- It is generally not allowed to do
Fragment
transactions from within theonLoadFinished()
callback. This is because the method can actually be called afterActivity.onSaveInstanceState()
. It’s an interesting race condition, but ultimately still up to you to handle. You could easily send a message to aHandler
bound to the main thread and have it handle theFragment
transaction as long as theActivity
is in the running state. - Depending on when you called
initLoader()
, the load may finish before UI components are initialized. I’ve seen this happen with apps usingFragment
s in particular. - Like the first bullet, you’ll need to be careful directly manipulating UI components from within
onLoadFinished()
as theActivity
orFragment
may no longer be in the running state and could result in an exception. - Any time
onLoadFinished()
is called you must discard any previous data provided by theLoader
. The new data being provide replaces it. However, don’t release the original resource (such as callingCursor.close()
) as theLoader
will handle the final cleanup of old data. - Remember that the
Loader
is already observing the data source for changes, so there is no need to set a data change observer for the data source (such asContentResolver.registerContentObserver()
).
Compatibility with pre-API 11
As mentioned previously, it is still possible to use an Android Loader
on targets running Android versions prior to Honeycomb. To do this, use the appcompat library v4 and derive your Activity
from FragmentActivity
. You can then use the android.support.v4.app.LoaderManager
, android.support.v4.content.AsyncTaskLoader
or android.support.v4.content.CursorLoader
.
Wrap Up
The Android Loader
API enables app authors more robust handling of data loading off the main thread. Unlike manually managing threads and coordinating with Activity
or Fragment
lifecycle, the LoaderManager
is tied to these core components and do much of the heavy lifting for us. Like most of the Android framework, the Loader
API is constructed to be generic so you can extend it for your own needs. The key to understanding the Loader
mechanism is understanding the objects involved and how/when they are called. So the next time you need to load any type of data or resource, consider easing your development efforts and debug pain level by using Loader
s.
The sources used in this article can be found at the following GitHub repository:
https://github.com/hiq-larryschiefer/BasicLoader.git