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
.
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:
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.
13.59.75.169