Combining Loaders

In the preceding sections we developed an AsyncTaskLoader that can load a single image thumbnail as a Bitmap, and a CursorLoader that loads a list of all available images on the device. Let's bring them together to create an app that tiles thumbnails of all the images on the device in a scrollable grid, performing all loading in the background.

Thanks to our CursorLoader, we already have access to the IDs of the images we need to load—we're displaying them as text—so we just need to pass those IDs to our ThumbnailLoader for it to asynchronously load the image for us.

Recall that ThumbnailLoader was set up to load one Bitmap and cache it forever (that is, until explicitly removed from LoaderManager). We want to change that so that a single ThumbnailLoader can first be constructed without a mediaId, and later be told to load an image with a particular mediaId.

public ThumbnailLoader(Context context) {
    super(context);
}

We'll enable ThumbnailLoader to load a new image instead of its current one, by setting a new mediaId. Since the bitmap is cached, just setting a new ID won't suffice—we also need to trigger a reload:

public void setMediaId(Integer newMediaId) {
    if ((mediaId != null) && (!mediaId.equals(newMediaId))
        this.mediaId = newMediaId;
        onContentChanged();
    }
}

What's this? onContentChanged is a method of the abstract Loader superclass, which will force a background load to occur if our Loader is currently in the "started" state. If we're currently "stopped", a flag will be set and a background load will be triggered next time the Loader is started. Either way, when the background load completes, onLoadFinished will be triggered with the new data.

We need to make a few changes to our onStartLoading method to correctly handle the case where we were "stopped" when onContentChanged was invoked. Let's remind ourselves of what it used to look like:

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

Now let's update onStartLoading to check if we need to reload our data, and respond appropriately:

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

The updated onStartLoading still delivers its data immediately—if it has any. It then calls takeContentChanged to see if we need to discard our cached Bitmap and load a new one. If takeContentChanged returns true, we invoke forceLoad to trigger a background load and redelivery.

Now we can cause our ThumbnailLoader to load and cache a different image, but a single ThumbnailLoader can only load and cache one image at a time, so we're going to need more than one active instance.

Let's walk through the process of modifying MediaCursorAdapter to initialize a ThumbnailLoader for each cell in the GridView, and to use those Loaders to asynchronously load the images and display them.

First, we can no longer rely on SimpleCursorAdapter, so we'll need to subclass CursorAdapter instead. We'll also need LoaderManager, which we'll pass to the constructor, and we'll grab LayoutInflater from the Context to use later when we create the View objects:

public class MediaCursorAdapter extends CursorAdapter {
    private LoaderManager mgr;
    private LayoutInflater inf;
    private int count;
    private List<Integer> ids;

    public MediaCursorAdapter(Context ctx, LoaderManager mgr) {
        super(ctx.getApplicationContext(), null, true);
        this.mgr = mgr;
        inf = (LayoutInflater) ctx.
            getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        ids = new ArrayList<Integer>();
    }

    @Override
    public View newView(
        Context ctx, Cursor crsr, ViewGroup parent) {
        return null; // todo
    }

    @Override
    public void bindView(View view, Context ctx, Cursor crsr) {
        // todo
    }
}

We have two methods to implement—newView and bindView. GridView will invoke newView until it has enough View objects to fill all of its visible cells, and from then on it will recycle these same View objects by passing them to bindView to be repopulated with data for a different cell as the grid scrolls. As a view scrolls out of sight, it becomes available for rebinding.

What this means for us is that we have a convenient method in which to initialize our ThumbnailLoadersnewView, and another convenient method in which to retask Loader to load a new thumbnail—bindView.

newView first inflates ImageView for the cell and gives it a unique ID, then passes it to a ThumbnailCallbacks class, which we'll meet in a moment. ThumbnailCallbacks is in turn used to initialize a new Loader, which shares the ID of the View. In this way we are initializing a new Loader for each visible cell in the grid:

@Override
public View newView(final Context ctx, Cursor crsr, ViewGroup vg){
    ImageView view = (ImageView)
        inf.inflate(R.layout.example3_cell, vg, false);
    view.setId(
        MediaCursorAdapter.class.hashCode() + count++);    
    mgr.initLoader(
        view.getId(), null, 
        new ThumbnailCallbacks(ctx, view));
    ids.add(view.getId());
    return view;
}

In bindView, we are recycling each existing View to update the image being displayed by that View. So the first thing we do is clear out the old Bitmap.

Next we look up the correct Loader by ID, extract the ID of the next image to load from the Cursor, and load it by passing the ID to a new method of ThumbnailLoadersetMediaId.

@Override
public void bindView(View view, final Context ctx, Cursor crsr) {
    ((ImageView)view).setImageBitmap(null);
    ThumbnailLoader loader = (ThumbnailLoader)
        mgr.getLoader(view.getId());
    loader.setMediaId(crsr.getInt(
        crsr.getColumnIndex(MediaStore.Images.Media._ID)));
}

We need to add one more method to our Adapter so that we can clean up ThumbnaiLoaders when we no longer need them. We'll call these ourselves when we no longer need these Loaders—for example, when our Activity is finishing:

public void destroyLoaders() () {
    for (Integer id : ids)
        mgr.destroyLoader(id);
}

That's our completed Adapter. Next, let's look at ThumbnailCallbacks which, as you probably guessed, is just an implementation of LoaderCallbacks:

public class ThumbnailCallbacks implements
LoaderManager.LoaderCallbacks<Bitmap> {
    private Context context;
    private ImageView image;

    public ThumbnailCallbacks(Context context, ImageView image) {
        this.context = context.getApplicationContext();
        this.image = image;
    }

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

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

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

The only interesting things ThumbnailCallbacks does is create an instance of ThumbnailLoader, and set a loaded Bitmap to its ImageView.

Our Activity is almost unchanged—we need to pass an extra parameter when instantiating MediaCursorAdapter and, to avoid leaking the Loaders it creates, we need to invoke the destroyLoaders method of MediaCursorAdapter in onPause or onStop, if the Activity is finishing:

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

    if (isFinishing()) {
        getSupportLoaderManager().destroyLoader(MS_LOADER);
        adapter.destroyLoaders();
    }
}

The full source code is available from the Packt Publishing website. Take a look at the complete source code to appreciate how little there actually is, and run it on a device to get a feel for just how much functionality Loaders give you for a relatively small effort!

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

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