Loading Remote Images in a ListView on Android

So this appears to be a common challenge:

I have a list of items in my mobile application. Each item in the list contains an image and a title. The data for the list comes from a remote web service, RESTful interface or other Internet connection and the images are provided in a separate call. I want the list to be responsive, so I’ll load the list and then in a separate thread I will update the images in the list as they are loaded from the network.

I struggled with this on the BlackBerry where there was little similar code to go on, but got it working. I just finished an Android app and discovered that we had the same pattern. Trying to re-use my BlackBerry code I found that the UI patterns don’t translate (beyond frameworks, Android recycles views while it’s rendering to keep memory-use low).

There are lots of posts on the web about how to do this for the Android, such as this thread.

Unfortunately all of those ran into the same problem: Sometimes the images would load and sometimes they wouldn’t. Sometimes the images in the list would be the wrong one for the list item when rendered. And some solutions only rendered images that were off screen.

Eventually I got this (mostly) working and am sharing the code below in the hopes that this will help. While this is vastly derived from Tom van Zummeren’s tutorial at jTeam, the key that made this work right was recognizing from Nazmul’s tutorial at developerlife.com  that I could use notifyDataSetChanged to redraw the list when new data came in.

Let me say that I went down many paths here and I’m not sure that the solution, adopted from Nazmul, is the “right” one because it uses and Adapter’s getView method to bind the data, rather than using a ViewBinder. I tried a ViewBinder approach at one point and might go back to that if there’s any reason to rewrite this.

In addition, my final change was to not re-use views at all which makes for somewhat poor performance. Has anyone had better luck with this?

The Code

I have a custom data object, MediaItem, that contains a URL to the image and the label to be displayed in the list. My main activity view contains a ListView instance in the layout.

<ListView android:id="@+id/playList"
  android:layout_width="fill_parent"
  android:layout_height="wrap_content"
  android:choiceMode="singleChoice"
  />

Each item in the listview is represented by a custom view.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="50dip"
 android:padding="10dip"
 >
 <ImageView android:id="@+id/icon"
 android:layout_width="50dp"
 android:layout_height="fill_parent"
 android:layout_marginRight="10dip"
 android:layout_alignParentLeft="true"
 />
 <TextView
 android:id="@+id/text"
 android:layout_width="fill_parent"
 android:layout_height="wrap_content"
 android:layout_toRightOf="@id/icon"
 android:gravity="center_vertical"
 android:textSize="12sp"
 android:textStyle="bold"
 android:ellipsize="marquee"
 android:singleLine="true"
 android:scrollHorizontally="true"
 />
</RelativeLayout>

In my onCreate method I set up and configure the list view to load the playlist (loaded elsewhere) from the data source through a custom adapter class.

@Override
public void onCreate(Bundle savedInstanceState) {
  listView = (ListView)findViewById(R.id.playList);
  listView.setAdapter(new MediaItemAdapter(this, R.layout.playlistitem, State.playlist));
}

In my code I actually did the loading in the onSurfaceCreated because (1) I wanted to use the spinner/progress indicator in the title bar while the playlist was loading data and because I want this to be reloaded by the playlist each time the app is brought into view. In most cases you probably wouldn’t want to do this.

The MediaItemAdapter class takes a List<MediaItem> which is our playlist.


public class MediaItemAdapter extends ArrayAdapter&lt;MediaItem&gt; {
  private final static String TAG = &quot;MediaItemAdapter&quot;;
  private int resourceId = 0;
  private LayoutInflater inflater;
  private Context context;

  private ImageThreadLoader imageLoader = new ImageThreadLoader();

  public MediaItemAdapter(Context context, int resourceId, List&lt;MediaItem&gt; mediaItems) {
    super(context, 0, mediaItems);
    this.resourceId = resourceId;
    inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    this.context = context;
  }

  @Override
  public View getView(int position, View convertView, ViewGroup parent) {

    View view;
    TextView textTitle;
    TextView textTimer;
    final ImageView image;

    view = inflater.inflate(resourceId, parent, false);

    try {
      textTitle = (TextView)view.findViewById(R.id.text);
      image = (ImageView)view.findViewById(R.id.icon);
    } catch( ClassCastException e ) {
      Log.e(TAG, &quot;Your layout must provide an image and a text view with ID's icon and text.&quot;, e);
      throw e;
    }

    MediaItem item = getItem(position);
    Bitmap cachedImage = null;
    try {
      cachedImage = imageLoader.loadImage(item.thumbnail, new ImageLoadedListener() {
      public void imageLoaded(Bitmap imageBitmap) {
      image.setImageBitmap(imageBitmap);
      notifyDataSetChanged();                }
      });
    } catch (MalformedURLException e) {
      Log.e(TAG, &quot;Bad remote image URL: &quot; + item.thumbnail, e);
    }

    textTitle.setText(item.name);

    if( cachedImage != null ) {
      image.setImageBitmap(cachedImage);
    }

    return view;
  }
}

The image is loaded in a separate thread. Several implementations spawned a new thread for each image. I don’t think that the 3G-or-worse speed of a mobile device is worth trying to download more than an image at a time and I’d rather have the first image show up as fast as possible so that the user sees that something is happening. So my implementation uses a queue and a single thread to load images that are in the queue until all images are loaded.

Note that in the adapter above I’m attaching an anonymous method to be called by the thread to set the bitmap image. The only variable used by that thread is a final reference to the ImageView for the item. I found that you can’t pass in a reference to the View for the item template or the ListView because of reuse and such. This was the most effective approach.

Advertisements

41 thoughts on “Loading Remote Images in a ListView on Android

  1. Do you know how to get that image view object? I saw and used your code. But that not loading images properly. One or two images not completed to losd. And gets the Outofmemory exception too. why? May you?

    • You’ll need to trace through your view hierarchy. Calling getChildAt(index) on a ListView returns the list view item (a Layout view) for the index from the top of the visible list. On that view object you can call findViewById to get the image view.

  2. this is what i get in my log cat:

    05-28 16:23:04.805: ERROR/AndroidRuntime(11202): Uncaught handler: thread Thread-16 exiting due to uncaught exception

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): java.lang.OutOfMemoryError: bitmap size exceeds VM budget

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:451)

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:503)

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at com.example.brown.ImageThreadLoader.readBitmapFromNetwork(ImageThreadLoader.java:156)

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at com.example.brown.ImageThreadLoader$QueueRunner.run(ImageThreadLoader.java:82)

    05-28 16:23:04.845: ERROR/AndroidRuntime(11202): at java.lang.Thread.run(Thread.java:1060)

    • I have no idea. I looked into RSS on Java a while ago and found another solution. Is that somehow related to your list view content?

  3. I’m facing the problem for binding listview about two thousands of record from internal database. I used AsyncTask and it’s find. But user need to wait about a minutes for loading data. Any suggest for this problem pls?

    • You should use a cursor to connect to your data adapter and only load records as they are scrolled into view. A typical list view on a handheld has about 4-6 items visible, so there’s no reason to find all 2000 or to keep those in memory. One option is to have a “load more” button in a footer for the list view that spins out a thread to load additional data. Even better would be to continuously load as the user scrolls so that (if timing and network speed are good) it seems that the list is all in memory.

  4. Hi Jeremy,

    I’m using ur code,its working nice for me. But only issue is after successful loading of all the images if i scrolls up 3 or 4 images are missing.Its happening each time in different places.Could please guide me about this?

    Thanks,
    Sai

  5. Tanks a lot for your post. It helped me too. I tried many many code template like yours, but this seems to be one of the best.

  6. Wow thanks, I’m very greatful for this code! It works perfectly with my list so far and was very easy to implement. I have not tried any other code then my own code so I guess I was lucky with google today =)

  7. Why sometimes does not return the bitmap? I need to upload 9 images but does not return the bitmap of all

    • Could be many things: inability to access the image (network connection, etc.), assigning the wrong data to the wrong object. Note that also instead of holding a reference to the View or Item, you should look it up by tag or something because views get recycled.

  8. -please give me answer my question
    how to upload images on server throw Api images size Min(10 kb) Max( 200 kb)

    • @santosh, I have no idea how you can “upload images to a server through an API”. That would depend on the API. Is this related to showing images in a ListView somehow?

  9. int dataColumnIndex = imagecursor.getColumnIndex(MediaStore.Images.Media.DATA);
    open the MediaStore on button click and select multiple image of save server threw web service(api)

    please help me

    • @santosh, I’m not familiar with the MediaStore API. I think you’ll find more experienced help if you post a well-written question on StackOverflow where many Android developers are available.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s