Building responsive apps with AsyncTaskLoader

AsyncTaskLoader is a Loader implementation that uses AsyncTasks to perform its background work, though this is largely hidden from us when we implement our own subclasses.

We don't need to trouble ourselves about the AsyncTasks—they are completely hidden by AsyncTaskLoader—but with what we learned earlier about AsyncTask, it is interesting to note that tasks are executed using AsyncTask.THREAD_POOL_EXECUTOR to ensure a high degree of concurrency when multiple Loaders are in use.

Loader is generically typed so, when we implement it, we need to specify the type of object that it will load—in our case android.graphics.Bitmap:

public class ThumbnailLoader extends AsyncTaskLoader<Bitmap> {
    // ...
}

The Loader abstract class requires a Context passed to its constructor, so we must pass a Context up the chain. We'll also need to know which thumbnail to load, so we'll also pass an identifier, mediaId:

private Integer mediaId;
public ThumbnailLoader(Context context, Integer mediaId){
    super(context);
    this.mediaId = mediaId;
}

We don't need to keep our own reference to the Context object—Loader exposes a getContext method, which we can call from anywhere in our class, where we might need a Context.

Tip

We can safely pass a reference to an Activity instance as the Context parameter, but we should not expect getContext() to return the same object!

Loaders potentially live much longer than a single Activity, so the Loader superclass only keeps a reference to the ApplicationContext to prevent memory leaks.

There are several methods we will need to override, which we'll work through one at a time. The most important is loadInBackground—the workhorse of our Loader, and the only method which does not run on the main thread:

@Override
public Bitmap loadInBackground() {
    //...
}

We're going to fetch a thumbnail Bitmap from MediaStore, which is fairly straightforward but is a potentially slow operation that is good to perform off the main thread.

The following diagram displays the Loader Lifecycle, showing callbacks invoked by Loader and a typical AsyncTaskLoader implementation:

Building responsive apps with AsyncTaskLoader

Loading a thumbnail entails some small delay due to blocking I/O reading from permanent storage, but it is also possible that the MediaStore does not yet have a thumbnail of the image we're requesting, in which case it must first read the original image and create a scaled thumbnail from it—a much more expensive operation that we definitely don't want to perform on the main thread!

Luckily we don't need to concern ourselves much with what's going on inside MediaStore—provided we call it on a background thread, we can afford to wait for it to do its thing:

@Override
public Bitmap loadInBackground() {
    ContentResolver res = getContext().getContentResolver();
    if (mediaId != null) {
        return MediaStore.Images.Thumbnails.getThumbnail(
            res, mediaId,
            MediaStore.Images.Thumbnails.MINI_KIND, null);
    }
    return null;
}

We'll want to cache a reference to the Bitmap object that we're delivering, so that any future calls can just return the same Bitmap immediately. We'll do this by overriding deliverResult:

@Override
public void deliverResult(Bitmap data) {
    this.data = data;
    super.deliverResult(data);
}

To make our Loader actually work, we still need to override a handful of lifecycle methods that are defined by the Loader base class. First and foremost is onStartLoading:

@Override
protected void onStartLoading() {
    if (data != null) {
        deliverResult(data);
    } else {
        forceLoad();
    }
}

Here we check our cache (data) to see if we have a previously loaded result that we can deliver immediately via deliverResult. If not, we trigger a background load to occur—we must do this, or our Loader won't ever load anything. All of this takes place on the main thread.

We now have a minimal working Loader implementation, but there is some housekeeping required if we want our Loader to play well with the framework.

First of all we need to make sure to clean up the Bitmap when our Loader is discarded. Loader provides a callback intended for that exact purpose—onReset.

@Override
protected void onReset() {
    if (data != null)
        data.recycle();
}

The framework will ensure that onReset is called when Loader is being discarded, which will happen when the app exits or when the Loader instance is explicitly discarded via LoaderManager.

There are two more lifecycle methods, which are important to implement correctly if we want our app to be as responsive as possible: onStopLoading and onCanceled (be careful of the spelling of onCanceled here versus onCancelled in most places).

The framework will tell us when it doesn't want us to waste cycles loading data by invoking the onStopLoading callback. It may still need the data we have already loaded though, and it may tell us to start loading again, so we should not clean up resources yet. In AsyncTaskLoader we'll want to cancel the background work if possible, so we'll just call the superclass cancelLoad method:

@Override
protected void onStopLoading() {
    cancelLoad();
}

Finally, we need to implement onCanceled to clean up any data that might be loaded in the background after a cancellation has been issued:

@Override
public void onCanceled(Bitmap data) {
    if (data != null)
        data.recycle();
}

Depending on the kind of data Loader produces, we may not need to worry about cleaning up the result of canceled work—ordinary Java objects will be cleaned up by the garbage collector when they are no longer referenced.

Bitmap, however, can be a tricky beast—older versions of Android store the bitmap data in native memory, outside of the normal object heap, requiring a call to recycle to avoid potential OutOfMemoryErrors.

So far so good—we have a Loader. Now we need to connect it to a client Activity or Fragment. We know that our Activity is going to load an image from an ID reference, and that it will need somewhere to display that image. Let's get these easy bits out of the way first:

public class ThumbnailActivity extends Activity {	
    private Integer mediaId;
    private ImageView thumb;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.thumb);
        thumb = (ImageView) findViewById(R.id.thumb);
        mediaId = getMediaIdFromIntent(getIntent());
    }
}

To launch this Activity, we will select an image in the gallery app and "send" it toour Activity. The getMediaIdFromIntent method will extract the ID from the Intent we receive:

private Integer getMediaIdFromIntent(Intent intent) {
    if (Intent.ACTION_SEND.equals(intent.getAction())) {
        Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
        return new Integer(uri.getLastPathSegment());
    } else {
        return null;
    }
}

In order to make our Activity available in the share via app selection, we need to specify that it can handle images in an ACTION_SEND intent by adding the appropriate intent filter in our AndroidManifest.xml file:

<activity
    android:name=".example1.ThumbnailActivity"
    android:label="@string/thumb_activity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <data android:mimeType="image/*" />
        ...
    </intent-filter>
</activity>

OK, we're ready to start fitting out our Activity to work with Loader! First, we need to obtain a reference to LoaderManager and call its initLoader method.

public abstract <D> Loader<D> initLoader(
    int id, Bundle args, 
    LoaderManager.LoaderCallbacks<D> callback);

The parameters to initLoader specify an int identifier for ThumbnailLoader. We can use this identifier to look up ThumbnaiLoader from LoaderManager whenever we need to interact with it. Remember that Loaders are not closely bound to the Activity lifecycle, so we can use this ID to retrieve the same Loader instance in a different Activity!

It's generally a good idea to use a public static int field to record the ID for each Loader, making it easy to use the correct ID from anywhere. A nice way to avoid accidentally re-using the same ID for different Loaders is to use the hashCode of a String name.

public static final int LOADER_ID = "thumb_loader".hashCode();

We can also pass a Bundle of values—the second parameter—to configure the Loader should we need to. For now we'll keep our example simple and just pass null.

The third parameter is an object that implements LoaderCallbacks. It's quite common to implement the callbacks directly in the Activity and pass this when initializing the Loader from within the Activity:

public class ThumbnailActivity extends Activity
implements LoaderManager.LoaderCallbacks<Bitmap> {
    public static final int LOADER_ID = 
        "thumb_loader".hashCode();
    private Integer mediaId;
    private ImageView thumb;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.thumb);
        thumb = (ImageView) findViewById(R.id.thumb);
        mediaId = getMediaIdFromIntent(getIntent());
        if (mediaId != null)
            getLoaderManager().initLoader(
               LOADER_ID, null, this);
    }

    @Override
    public Loader<Bitmap> onCreateLoader(
        int id, Bundle bundle){
        return null; // todo
    }
    
    @Override
    public void onLoadFinished(
        Loader<Bitmap> loader, Bitmap bitmap){
        // todo
    }

    @Override
    public void onLoaderReset(Loader<Bitmap> loader) {
        // todo
    }
}

When we initialize ThumbnaiLoader via LoaderManager's initLoader method, it will either return an existing Loader with the given ID (LOADER_ID) or, if it doesn't yet have a Loader with that ID, it will invoke the first of the LoaderCallbacks methods—onCreateLoader. This is where we get to choose which type of Loader to instantiate:

@Override
public Loader<Bitmap> onCreateLoader(
    int loaderId, Bundle bundle) {
    return new ThumbnailLoader(
        getApplicationContext(), mediaId);
}

Notice that the parameters to onCreateLoader receive the same ID and bundle values we passed to the initLoader call.

The implementation of onLoadFinished must take the loaded Bitmap and insert it into our ImageView:

@Override
public void onLoadFinished(Loader<Bitmap> loader, Bitmap bitmap) {
    thumb.setImageBitmap(bitmap);
}

We're almost done—all that remains is to do any necessary clean up in onLoaderReset:

@Override
protected void onReset() {
    Log.i(LaunchActivity.TAG, getId() + " onReset");

    // if we have a bitmap, make sure it is
    // recycled asap.
    if (data != null) {
        data.recycle();

        data = null;
    }
}

Phew! We've loaded an image in the background and displayed it when loading is completed. When compared with AsyncTask, things here are more complicated—we've had to write more code and deal with more classes, but the payoff is that the data is cached for use across Activity restarts and can be used from other Fragments or Activities.

In the next section we'll implement CursorLoader to efficiently retrieve and display a list of all images on the device.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.79.146