Safe, standard way to load images in ListView on a different thread?
- by Po
Before making this question, I have searched and read these ones:
http://stackoverflow.com/questions/541966/android-how-do-i-do-a-lazy-load-of-images-in-listview
http://stackoverflow.com/questions/1409623/android-issue-with-lazy-loading-images-into-a-listview
My problem is I have a ListView, where:
Each row contains an ImageView, whose
content is to be loaded from the
internet
Each row's view is recycled as in
ApiDemo's List14
What I want ultimately:
Load images lazily, only when the user
scrolls to them
Load images on different thread(s) to
maintain responsiveness
My current approach:
In the adapter's getView() method, apart from
setting up other child views, I launch a
new thread that loads the Bitmap from
the internet. When that loading thread
finishes, it returns the Bitmap to be set on the ImageView (I do this using AsyncTask or Handler).
Because I recycle ImageViews, it may
be the case that I first want to set
a view with Bitmap#1, then later want
to set it to Bitmap#2 when the user
scrolls down. Bitmap#1 may happen to
take longer than Bitmap#2 to load, so
it may end up overwriting Bitmap#2 on
the view. I solve this by maintaining
a WeakHashMap that remembers the last Bitmap I want
to set for that view.
Below is somewhat a pseudocode for my current approach. I've ommitted other details like caching, just to keep the thing clear.
public class ImageLoader {
// keeps track of the last Bitmap we want to set for this ImageView
private static final WeakHashMap<ImageView, AsyncTask> assignments
= new WeakHashMap<ImageView, AsyncTask>();
/** Asynchronously sets an ImageView to some Bitmap loaded from the internet */
public static void setImageAsync(final ImageView imageView, final String imageUrl) {
// cancel whatever previous task
AsyncTask oldTask = assignments.get(imageView);
if (oldTask != null) {
oldTask.cancel(true);
}
// prepare to launch a new task to load this new image
AsyncTask<String, Integer, Bitmap> newTask = new AsyncTask<String, Integer, Bitmap>() {
protected void onPreExecute() {
// set ImageView to some "loading..." image
}
protected Bitmap doInBackground(String... urls) {
return loadFromInternet(imageUrl);
}
protected void onPostExecute(Bitmap bitmap) {
// set Bitmap if successfully loaded, or an "error" image
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.error);
}
}
};
newTask.execute();
// mark this as the latest Bitmap we want to set for this ImageView
assignments.put(imageView, newTask);
}
/** returns (Bitmap on success | null on error) */
private Bitmap loadFromInternet(String imageUrl) {}
}
Problem I still have: what if the Activity gets destroyed while some images are still loading?
Is there any risk when the loading
thread calls back to the ImageView
later, when the Activity is already
destroyed?
Moreover, AsyncTask has some global
thread-pool underneath, so if lengthy
tasks are not canceled when they're
not needed anymore, I may end up
wasting time loading things users
don't see. My current design of
keeping this thing globally is too
ugly, and may eventually cause some
leaks that are beyond my
understanding. Instead of making
ImageLoader a singleton like this,
I'm thinking of actually creating
separate ImageLoader objects for
different Activities, then when an
Activity gets destroyed, all its
AsyncTask will be canceled. Is this
too awkward?
Anyway, I wonder if there is a safe and standard way of doing this in Android. In addition, I don't know iPhone but is there a similar problem there and do they have a standard way to do this kind of task?
Many thanks.