Multi-threading images from the web into an Adapter
When working with ListViews or GridViews inside Android apps, you may need to load dynamic content, such as photos, from the web into these views. If you're not multi-threading your UI will hang. This is because everything is running on a single thread and the latency of the network will make your app unresponsive. Most of the time, if you're retrieving anything from the web it's better to be multi-threading to keep your UI responsive.
GridViews and ListViews (among others) use Adapters to connect data to your views. There's explanation on how to use Adapters inside a GridView on the Android Developer's site. While this example is great for static content, you'll run into problems when loading images from the web.
I'm currently building an Android app that's pretty much my photoalbum I have on my site. I'm loading my thumbnails into a GridView as shown below and from here you can tap on a thumbnail to see a bigger version. Screenshot of my photoalbum app, the GridView loading external images
While the GridView example on the Android Developer's site is great for loading static content, I needed to load images from the web, which means I should be multi-threading. I've done this inside my Adapter and then set my Adapter to my GridView. Below is my ImageAdapter class. I thought the best way to explain this would be explanations in the code itself, check it out..
package ca.ryac.photoalbum.adapters;
import ...
public class ImageAdapter extends BaseAdapter {
private Context context;
// stores all data for album..
private ArrayList<HashMap> photos;
// my folder I'm currently loading from..
private String folder;
// views stores all the loaded ImageViews linked to their
// position in the GridView; hence in
// the HashMap..
private HashMap views;
public ImageAdapter (Context c, String f, ArrayList<HashMap> p) {
context = c;
// I need to hold a reference to my current album folder..
folder = f;
// photos stores all of my data for the album, this includes
// the filename, some other stuff I'm not using..
photos = p;
// 'views' is a HashMap that holds all
// of the loaded ImageViews linked to their position
// inside the GridView. it's used for checking to see if
// the particular ImageView has already been loaded
// (inside the getView method) and if not, creates the
// new ImageView and stores it in the HashMap along with
// its position..
views = new HashMap();
}
@Override
public int getCount() {
return photos.size();
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
ImageView v;
// get the ImageView for this position in the GridView..
v = views.get(position);
// this ImageView might not be created yet..
if (v == null) {
Log.d(Constants.TAG, "This view is not created. create it.");
// create a new ImageView..
v = new ImageView(context);
v.setLayoutParams(new GridView.LayoutParams(110, 110));
v.setPadding(10, 10, 10, 10);
// I'm setting a default image here so that you don't
// see black nothingness.. (just using an icon that
// comes with the Android SDK)
v.setImageResource(android.R.drawable.ic_menu_gallery);
// get the filename that this ImageView will hold..
String file = photos.get(position).get("file").toString();
// pass this Bundle along to the LoadImage class,
// which is a subclass of Android's utility class
// AsyncTask. Whatever happens in this class is
// on its own thread.. the Bundle passes
// the file to load and the position the photo
// should be placed in the GridView..
Bundle b = new Bundle ();
b.putString("file", file);
b.putInt("pos", position);
// just a test to make sure that the position and
// file name are matching before and after the
// image has loaded..
Log.d(Constants.TAG, "*before: " + b.getInt("pos") + " | " + b.getString("file"));
// this executes a new thread, passing along the file
// to load and the position via the Bundle..
new LoadImage().execute(b);
// puts this new ImageView and position in the HashMap.
views.put(position, v);
}
// return the view to the GridView..
// at this point, the ImageView is only displaying the
// default icon..
return v;
}
// this is the class that handles the loading of images from the cloud
// inside another thread, separate from the main UI thread..
private class LoadImage extends AsyncTask {
@Override
protected Bundle doInBackground(Bundle... b) {
// get the file that was passed from the bundle..
String file = b[0].getString("file");
// fetchPhoto is a helper method to get the photo..
// returns a Bitmap which we'll place inside the
// appropriate ImageView component..
Bitmap bm = fetchPhoto(file);
// now that we have the bitmap (bm), we'll
// create another bundle to pass to 'onPostExecute'.
// this is the method that is called at the end of
// our task. like a callback function..
// this time, we're not passing the filename to this
// method, but the actual bitmap, not forgetting to
// pass the same position along..
Bundle bundle = new Bundle();
bundle.putParcelable("bm", bm);
bundle.putInt("pos", b[0].getInt("pos"));
bundle.putString("file", file); // this is only used for testing..
return bundle;
}
@Override
protected void onPostExecute(Bundle result) {
super.onPostExecute(result);
// just a test to make sure that the position and
// file name are matching before and after the
// image has loaded..
Log.d(Constants.TAG, "*after: " + result.getInt("pos") + " | " + result.getString("file"));
// here's where the photo gets put into the
// appropriate ImageView. we're retrieving the
// ImageView from the HashMap according to
// the position..
ImageView view = views.get(result.getInt("pos"));
// then we set the bitmap into that view. and that's it.
view.setImageBitmap((Bitmap) result.getParcelable("bm"));
}
}
// this is a helper method to retrieve the photo from the cloud..
private Bitmap fetchPhoto (String file) {
Bitmap bm = null;
String path = "http://www.path_to_photoalbums_here.ca/" + file;
Log.d(Constants.TAG, "path: " + path);
try {
final HttpParams httpParameters = new BasicHttpParams();
// Set the timeout in milliseconds until a connection is established.
HttpConnectionParams.setConnectionTimeout(httpParameters, 7000);
// Set the default socket timeout (SO_TIMEOUT)
// in milliseconds which is the timeout for waiting for data.
HttpConnectionParams.setSoTimeout(httpParameters, 10000);
final HttpClient client = new DefaultHttpClient(httpParameters);
final HttpResponse response = client.execute(new HttpGet(path));
final HttpEntity entity = response.getEntity();
final InputStream imageContentInputStream = entity.getContent();
// wrapping the imageContentInputStream with FlushedInputStream.
bm = BitmapFactory.decodeStream(new FlushedInputStream (imageContentInputStream));
}
catch (Exception e) {
Log.e(Constants.TAG, e.getMessage(), e);
}
return bm;
}
}
And inside my Activity class, I simply connect the Adapter to the GridView:
private void setAdapter() {
grid.setAdapter(new ImageAdapter(this, folder, photos));
}
I did run into another problem with loading the images into my app, not only in my GridView but also when viewing the bigger version of the photo. The bitmap would not display and no obvious errors came up. Looking at the logcat I found this: "skia decoder->decode returned false". I've read that this happens over slow connections but my connection seems pretty fast so after searching around I found a solution to the 'decoder->decode returned false' problem here. There are actually two solutions explained. Try both if the first method didn't work for you -- I was originally using the second method while getting the 'decoder->decode returned false' problem. I then tried the first method and that worked.
I hope this helps to shed some light on multi-threading images from the cloud into your Adapters for various Views. If you see areas of improvement please let me know!