The Loader Framework

One perpetual problem in Android development is getting work to run outside the main application thread. Every millisecond we spend on the main application thread is a millisecond that our UI is frozen and unresponsive. Disk I/O, in particular, is a common source of such slowdowns, particularly since this is one place where the emulator typically out-performs actual devices. While disk operations rarely get to the level of causing an “application not responding” (ANR) dialog to appear, they can make a UI “janky”.

Android 3.0 introduced a new framework to help deal with loading bulk data off of disk, called “loaders”. The hope is that developers can use loaders to move database queries and similar operations into the background and off the main application thread. That being said, loaders themselves have issues, not the least of which is the fact that it is new to Android 3.0 and therefore presents some surmountable challenges for use in older Android devices.

This chapter will outline the programming pattern loaders are designed to solve, how to use loaders (both built-in and third-party ones) in your activities, and how to create your own loaders for scenarios not already covered.

Prerequisites

Understanding this chapter requires that you have read the chapters on:

Cursors: Issues with Management

Android had the concept of “managed cursors” in Android 1.x/2.x. A managed Cursor was one that an Activity… well… manages. More specifically:

  1. When the activity was stopped, the managed Cursor was deactivated, freeing up all of the memory associated with the result set, and thereby reducing the activity’s heap footprint while it was not in the foreground
  2. When the activity was restarted, the managed Cursor was requeried, to bring back the deactivated data, along the way incorporating any changes in that data that may have occurred while the activity was off-screen
  3. When the activity was destroyed, the managed Cursor was closed.

This is a delightful set of functionality. Cursor objects obtained from a ContentProvider via managedQuery() were automatically managed; a Cursor from SQLiteDatabase could be managed by startManagingCursor().

The problem is that the requery() operation that was performed when the activity is restarted is executed on the main application thread. As has been noted elsewhere in the book, you really do not want to do disk I/O on the main application thread, as it freezes the UI and causes jank. This is particularly true for database I/O, where you may not know in advance exactly how much data you will get back or how long the query will take.

Introducing the Loader Framework

The Loader framework was designed to solve three issues with the old managed Cursor implementation:

There are three major pieces to the Loader framework: LoaderManager, LoaderCallbacks, and the Loader itself.

LoaderManager

LoaderManager is your gateway to the Loader framework. You obtain one by calling getLoaderManager() (or getSupportLoaderManager(), as is described later in this chapter). Via the LoaderManager you can initialize a Loader, restart that Loader (e.g., if you have a different query to use for loading the data), etc.

LoaderCallbacks

Much of your interaction with the Loader, though, comes from your LoaderCallbacks object, such as your activity if that is where you elect to implement the LoaderCallbacks interface. Here, you will implement three “lifecycle” methods for consuming a Loader:

  1. onCreateLoader() is called when your activity requests that a LoaderManager initialize a Loader. Here, you will create the instance of the Loader itself, teaching it whatever it needs to know to go load your data
  2. onLoadFinished() is called when the Loader has actually loaded the data — you can take those results and pour them into your UI, such as calling swapCursor() on a CursorAdapter to supply the fresh Cursor’s worth of data
  3. onLoaderReset() is called when you should stop using the data supplied to you in the last onLoadFinished() call (e.g., the Cursor is going to be closed), so you can arrange to make that happen (e.g., call swapCursor(null) on a CursorAdapter)

When you implement the LoaderCallbacks interface, you will need to provide the data type of whatever it is that your Loader is loading (e.g., LoaderCallbacks<Cursor>). If you have several loaders returning different data types, you may wish to consider implementing LoaderCallbacks on multiple objects (e.g., instances of anonymous inner classes), so you can take advantage of the type safety offered by Java generics, rather than implementing LoaderCallbacks<Object> or something to that effect.

Loader

Then, of course, there is Loader itself.

Consumers of the Loader framework will use some concrete implementation of the abstract Loader class in their LoaderCallbacks onCreateLoader() method. API Level 11 introduced only one concrete implementation: CursorLoader, designed to perform queries on a ContentProvider, and described in a later section.

Choosing an Implementation

Loader and its related classes were introduced in API Level 11 (Android 3.0). If your minSdkVersion is 11 or higher, you can use loaders “naturally” via the standard implementation.

If your minSdkVersion is below 11, the Android Support package offers its own implementation of Loader and the other classes. However, to use it, you will need to work within four constraints:

These limitations are the same ones that you will encounter when using fragments on older devices. Hence, while loaders and fragments are not really related, you may find yourself adopting both of them at the same time, as part of incorporating the Android Support package into your project.

Using CursorLoader

Let’s start off by examining the simplest case: using a CursorLoader to asynchronously populate and update a Cursor retrieved from a ContentProvider. This is illustrated in the Loaders/ConstantsLoader sample project, which is the same show-the-list-of-gravity-constants sample application that we examined previously, updated to use the Loader framework. Note that this project does not use the Android Support package and therefore only supports API Level 11 and higher.

In onCreate(), rather than executing a managedQuery() to retrieve our constants, we ask our LoaderManager to initialize a loader, after setting up our SimpleCursorAdapter on a null Cursor:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    adapter=new SimpleCursorAdapter(this,
                          R.layout.row, null,
                          new String[] {Provider.Constants.TITLE,
                                          Provider.Constants.VALUE},
                          new int[] {R.id.title, R.id.value});
    
    setListAdapter(adapter);
    registerForContextMenu(getListView());
    getLoaderManager().initLoader(0, null, this);
  }

(from Loaders/ConstantsLoader/app/src/main/java/com/commonsware/android/loader/ConstantsBrowser.java)

Using a null Cursor means we will have an empty list at the outset, a problem we will rectify shortly.

The initLoader() call on LoaderManager (retrieved via getLoaderManager()) takes three parameters:

The first time you call this for a given identifier, your onCreateLoader() method of the LoaderCallbacks will be called. Here, you need to initialize the Loader to use for this identifier. You are passed the identifier plus the Bundle (if any was supplied). In our case, we want to use a CursorLoader:

  public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
    return(new CursorLoader(this, Provider.Constants.CONTENT_URI,
                            PROJECTION, null, null, null));
  }

(from Loaders/ConstantsLoader/app/src/main/java/com/commonsware/android/loader/ConstantsBrowser.java)

CursorLoader takes a Context plus all of the parameters you would ordinarily use with managedQuery(), such as the content provider Uri. Hence, converting existing code to use CursorLoader means converting your managedQuery() call into an invocation of the CursorLoader constructor inside of your onCreateLoader() method.

At this point, the CursorLoader will query the content provider, but do so on a background thread, so the main application thread is not tied up. When the Cursor has been retrieved, it is supplied to your onLoadFinished() method of your LoaderCallbacks:

  public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    adapter.swapCursor(cursor);
  }

(from Loaders/ConstantsLoader/app/src/main/java/com/commonsware/android/loader/ConstantsBrowser.java)

Here, we call the new swapCursor() available on CursorAdapter, to replace the original null Cursor with the newly-loaded Cursor.

Your onLoadFinished() method will also be called whenever the data represented by your Uri changes. That is because the CursorLoader is registering a ContentObserver, so it will find out about data changes and will automatically requery the Cursor and supply you with the updated data.

Eventually, onLoaderReset() will be called. You are passed a Cursor object that you were supplied previously in onLoadFinished(). You need to make sure that you are no longer using that Cursor at this point — in our case, we swap null back into our CursorAdapter:

  public void onLoaderReset(Loader<Cursor> loader) {
    adapter.swapCursor(null);
  }

(from Loaders/ConstantsLoader/app/src/main/java/com/commonsware/android/loader/ConstantsBrowser.java)

And that’s pretty much it, at least for using CursorLoader. Of course, you need a content provider to make this work, and creating a content provider involves a bit of work.

What Else Is Missing?

The Loader framework does an excellent job of handling queries in the background. What it does not do is help us with anything else that is supposed to be in the background, such as inserts, updates, deletes, or creating/upgrading the database. It is all too easy to put those on the main application thread and therefore possibly encounter issues. Moreover, since the thread(s) used by the Loader framework are an implementation detail, we cannot use those threads ourselves necessarily for the other CRUD operations.

What Happens When…?

Here are some common development scenarios and how the Loader framework addresses them.

… the Data Behind the Loader Changes?

According to the Loader documentation, “They monitor the source of their data and deliver new results when the content changes”.

The documentation is incorrect.

A Loader can “monitor the source of their data and deliver new results when the content changes”. There is nothing in the framework that requires this behavior. Moreover, there are some cases where it is clearly a bad idea to do this — imagine a Loader loading data off of the Internet, needing to constantly poll some server to look for changes.

The documentation for a Loader implementation should tell you the rules. Android’s built-in CursorLoader does deliver new results, by means of a behind-the-scenes ContentObserver. However, it is not automatic that a Loader deliver new results, and it may be impractical for a Loader to deliver new results.

… the Configuration Changes?

The managed Cursor system that the Loader framework replaces would automatically requery() any managed Cursor objects when an activity was restarted. This would update the Cursor in place with fresh data after a configuration change. Of course, it would do that on the main application thread, which was not ideal.

Your Loader objects are retained across the configuration change automatically. Barring bugs in a specific Loader implementation, your Loader should then hand the new activity instance the data that was retrieved on behalf of the old activity instance (e.g., the Cursor).

Hence, you do not have to do anything special for configuration changes.

… the Activity is Destroyed?

Another thing the managed Cursor system gave you was the automatic closing of your Cursor when the activity was destroyed. The Loader framework does this as well, by triggering a reset of the Loader, which obligates the Loader to release any loaded data.

… the Activity is Stopped?

The final major feature of the managed Cursor system was that it would deactivate() a managed Cursor when the activity was stopped. This would release all of the heap space held by that Cursor while it was not on the screen. Since the Cursor was refreshed as part of restarting the activity, this usually worked fairly well and would help minimize pressure on the heap.

Alas, this does not appear to be supported by the Loader framework. The Loader is reset when an activity is destroyed, not stopped. Hence, the Loader data will continue to tie up heap space even while the activity is not in the foreground.

For many activities, this should not pose a problem, as the heap space consumed by their Cursor objects is modest. If you have an activity with a massive Cursor, though, you may wish to consider what steps you can take on your own, outside of the Loader framework, to help with this.

Writing a Custom Loader

Perhaps, despite the above issues, and despite the author’s assertion that the Loader framework is a failed abstraction, you want to implement a custom Loader.

You have two main choices for doing that:

If your API can work either way — synchronously or asynchronously – either option works. In terms of getting the Loader implementation right, you may want to use AsyncTaskLoader, as it will ensure that everything is delivered on the right thread. On the other hand, you may have greater control over the nature of the asynchronous work using the API’s native asynchronous capability, such as configuring a thread pool.

The HTTP/RetroLoader sample project is a clone of the HTTP/Retrofit sample app from the chapter on Internet access. However, this time, the Retrofit work to load the most recent android Stack Overflow questions will be mediated by a QuestionsLoader.

Changing the Retrofit Interface

Retrofit offers both synchronous and asynchronous APIs. For the purposes of this sample, we will use the synchronous API, to see how one might implement an AsyncTaskLoader. That, in turn, requires us to modify StackOverflowInterface, having questions() return the SOQuestions directly, rather than by using a callback:

package com.commonsware.android.retrofit;

import retrofit.http.GET;
import retrofit.http.Query;

public interface StackOverflowInterface {
  @GET("/2.1/questions?order=desc&sort=creation&site=stackoverflow")
  SOQuestions questions(@Query("tagged") String tags);
}

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/StackOverflowInterface.java)

Otherwise, this is unchanged from the original edition of this sample.

Implementing QuestionsLoader

QuestionsLoader is an implementation of AsyncTaskLoader. The documentation for Loader and AsyncTaskLoader leave a lot to be desired. QuestionsLoader is based on “triangulation” between the installed-applications loader included in the AsyncTaskLoader documentation and the source code to CursorLoader.

Loader uses Java generics. Its declaration requires the type of content being loaded by the Loader. So, QuestionsFragment is a Loader of SOQuestions:

public class QuestionsLoader extends AsyncTaskLoader<SOQuestions> {
  final private StackOverflowInterface so;
  private SOQuestions lastResult;

  public QuestionsLoader(Context context) {
    super(context);

    RestAdapter restAdapter=
      new RestAdapter.Builder().setEndpoint("https://api.stackexchange.com")
        .build();

    so=restAdapter.create(StackOverflowInterface.class);
  }

  @Override
  protected void onStartLoading() {
    super.onStartLoading();

    if (lastResult!=null) {
      deliverResult(lastResult);
    }
    else {
      forceLoad();
    }
  }

  @Override
  synchronized public SOQuestions loadInBackground() {
    if (isLoadInBackgroundCanceled()) {
      throw new OperationCanceledException();
    }

    return(so.questions("android"));
  }

  @Override
  public void deliverResult(SOQuestions data) {
    if (isReset()) {
      // actual cleanup, if any
    }

    lastResult=data;

    if (isStarted()) {
      super.deliverResult(data);
    }
  }

  @Override
  protected void onStopLoading() {
    super.onStopLoading();

    cancelLoad();
  }

  @Override
  protected void onReset() {
    super.onReset();

    onStopLoading();
    // plus any actual cleanup
  }
}

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

The Constructor

A Loader needs to have a constructor that takes a Context as a parameter. So, QuestionsLoader has one, that chains to the superclass constructor, plus sets up the StackOverflowInterface using Retrofit:

  public QuestionsLoader(Context context) {
    super(context);

    RestAdapter restAdapter=
      new RestAdapter.Builder().setEndpoint("https://api.stackexchange.com")
        .build();

    so=restAdapter.create(StackOverflowInterface.class);
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

Loading the Questions

The real work for loading the questions comes in loadInBackground(). Subclasses of AsyncTaskLoader need to implement this to return the content being loaded. As the name suggests, loadInBackground() is called on a background thread, so you can take time here.

The key piece of the loadInBackground() of QuestionsFragment is the call to questions() on the StackOverflowInterface, to retrieve the desired questions:

  @Override
  synchronized public SOQuestions loadInBackground() {
    if (isLoadInBackgroundCanceled()) {
      throw new OperationCanceledException();
    }

    return(so.questions("android"));
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

However, it is possible that before loadInBackground() is called, that something cancels the AsyncTask in the AsyncTaskLoader. A subclass of AsyncTaskLoader needs to check isLoadInBackgroundCanceled() in loadInBackground(), and throw an OperationCanceledException if isLoadInBackgroundCanceled() returns true.

Delivering Results

Not only do subclasses of AsyncTaskLoader have to load the content, they also have to cache the results of the previous load. So, QuestionsLoader has a lastResult field, holding onto an SOQuestions object.

In deliverResult(), we need to do three things:

  1. If isReset() returns true, indicating that the Loader was reset, we need to clean up anything associated with the previous load results
  2. We need to cache the new load results, which are passed into deliverResult() as a parameter
  3. If the loader is started (isStarted() returns true), we need to chain to the superclass implementation of deliverResult() to actually deliver the data to the LoaderCallbacks implementation:

  @Override
  public void deliverResult(SOQuestions data) {
    if (isReset()) {
      // actual cleanup, if any
    }

    lastResult=data;

    if (isStarted()) {
      super.deliverResult(data);
    }
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

Here, lastResult is just a POJO holding onto other POJOs, so there is nothing specific for us to do in case the loader was reset. If our results were a Cursor, a Bitmap, or other objects with clear “close” or “release” semantics, you might do that work if onReset() returned true.

Starting, Stopping, and Resetting

You also need to implement three additional methods.

First is onStartLoading(). Here is where we use the cached result, delivering it via deliverResults(). If we do not have a cached result, we need to call forceLoad() to trigger the AsyncTask which, in turn, triggers loadInBackground() and the rest of the work to actually retrieve the results:

  @Override
  protected void onStartLoading() {
    super.onStartLoading();

    if (lastResult!=null) {
      deliverResult(lastResult);
    }
    else {
      forceLoad();
    }
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

There is a corresponding onStopLoading(), where we need to call cancelLoad(), to cancel the AsyncTask:

  @Override
  protected void onStopLoading() {
    super.onStopLoading();

    cancelLoad();
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

Finally, there is onReset(), where we need to call onStopLoading() (as loading should stop if the Loader is reset), plus do any cleanup of our cached results as needed:

  @Override
  protected void onReset() {
    super.onReset();

    onStopLoading();
    // plus any actual cleanup
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsLoader.java)

Using QuestionsLoader

You might think that with 70-odd lines of QuestionsLoader code that we would have a corresponding savings in QuestionsFragment.

Alas, no, though it is a bit shorter.

QuestionsFragment now implements LoaderCallbacks instead of Retrofit’s Callback interface:

public class QuestionsFragment extends ListFragment implements
    LoaderManager.LoaderCallbacks<SOQuestions> {

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsFragment.java)

In the original QuestionsFragment, we fired off the asynchronous Retrofit work in onCreateView(), and we processed the results in the success() and failure() methods.

Now, we just need to call initLoader(), in this case from onViewCreated():

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    getLoaderManager().initLoader(0, null, this);
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsFragment.java)

Then, our LoaderCallbacks methods create the QuestionsLoader and apply the results:

  @Override
  public Loader<SOQuestions> onCreateLoader(int id, Bundle args) {
    return(new QuestionsLoader(getActivity()));
  }

  @Override
  public void onLoadFinished(Loader<SOQuestions> loader,
                             SOQuestions data) {
    setListAdapter(new ItemsAdapter(data.items));
  }

  @Override
  public void onLoaderReset(Loader<SOQuestions> loader) {
    setListAdapter(null);
  }

(from HTTP/RetroLoader/app/src/main/java/com/commonsware/android/retrofit/QuestionsFragment.java)

Considering the Loader Contract

As noted earlier in this chapter, there are three key pieces of the Loader contract: asynchronicity, data retention, and automatic delivery of updates on content changes.

So, how does QuestionsLoader stack up?

This is a common gap with loader implementations. Occasionally, we may be in position to find out when the content changes. More often, we are not, or the work to find out about content changes has to be handled by a much larger subsystem, beyond a simple Loader subclass.

For example, if we really wanted to have QuestionsLoader automatically deliver a fresh SOQuestions object when new Stack Overflow android questions were asked, we could: use some sort of timing mechanism (e.g., ScheduledExecutorService) to poll the Stack Exchange API every so often. However, then we would need to implement some sort of “diff” algorithm to determine if relevant data changed in the JSON response, so we knew to deliver a fresh SOQuestions to clients. However, our polling period would be somewhat arbitrary. Frequent polling would consume a fair amount of battery and bandwidth as well.