Getting Android ListView Right the First Time

ListView is an Android UI element commonly used when you want to display a scrollable list of items. Unless you have a simple, static list of items, you’ll probably end up subclassing BaseAdapater in order to provide content for Android ListView. The basic process of doing this is fairly straightforward, but there are a few mistakes that are easy to make if you’re not careful.

Mistake 1: Updating the UI from a Background Thread

Most UI frameworks require updating user interface elements only from the main thread, and Android is no exception. However, it may not be immediately obvious that you’re modifying the UI from a background thread. If your list requires some network calls or heavy processing, you’ll certainly be offloading that work to another thread.

ListView contains some additional guards that will throw a fatal exception if they detect that you’ve updated the underlying data model from a background thread. But if you have a race condition or other timing issue, you may not ever see the exception under normal development conditions. You’ll know it happened when your app crashes and you see a message like this in logcat:

Fatal Exception: java.lang.IllegalStateException
The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. Make sure your adapter calls notifyDataSetChanged() when its content changes.

The error description is actually somewhat helpful in resolving the issue:

  • Make sure ListView’s adapter only changes its contents on the main thread
  • Call notifyDataSetChanged() immediately after updating contents

And “immediately” really does mean immediately–if you wait even until the next event loop, ListView will have a chance to redraw and therefore notice that the adapter’s contents have changed.  Note that it is not sufficient just to make sure you are calling notifyDataSetChanged() on the main thread; you need to also make sure that although your background work is done on a different thread, it does not update your adapter from that thread.

Mistake 2: Not Using a ViewHolder

In order to have the best user experience, you’ll want to keep scrolling performance fast. An easy way to kill scrolling performance is to do a lot of work in the adapter’s getView() method. Like getCount(), you want this method to return as quickly as possible. The process of configuring a row usually involves inflating a view, finding the relevant subviews (like TextView, ImageView, etc.), and populating them with data for the row.

ListView helps a lot in this regard by recycling views. If it has a view available to reuse, it will pass it in the convertView parameter. This saves you from having to inflate a new view, but you should also avoid looking up all of the subviews again. A simple ViewHolder class can keep references to these subviews when you first create the view.


public class MyRowViewHolder {
    TextView titleView;
    TextView subtitleView;
    ImageView photoImage;
}

Then you just need to have the getView() method associate an instance of the ViewHolder with the row:


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

    if (convertView == null) {
        View rowView = inflater.inflate(R.id.my_row_view, parent, false);
        rowViewHolder = new MyRowViewHolder();
        rowViewHolder.titleView = (TextView)rowView.findViewById(R.id.title_textview);
        rowViewHolder.subtitleView = (TextView)rowView.findViewById(R.id.subtitle_textview);
        rowViewHolder.photoImage = (ImageView)rowView.findViewById(R.id.photo_imageview);
        rowView.setTag(rowViewHolder);
    } else {
        rowViewHolder = (MyRowViewHolder)rowView.getTag();
    }

    // Update the contents of rowViewHolder's subviews here
}

Mistake 3: Inflating Views and Passing Null for the “Root” Parameter

This applies to more than just ListView, but this mistake is especially easy to make since the adapter is just inflating a row and returning it to the ListView. The Android documentation says that the root parameter is optional, but does not discuss the consequences of leaving it out.

When building a layout in XML, you can specify attributes such as layout_width on an element like a TextView. When the layout is inflated, a subclass of ViewGroup.LayoutParams is used to represent these layout parameters. For instance, a TextView within a LinearLayout will have LinearLayout.LayoutParams. But if the layout inflater does not have a root view group to use as a reference, it cannot know which LayoutParams subclass to use.

As a result, your inflated view may not have all of the layout parameters you are expecting. It’s also entirely possible that the defaults coincide with the values you specified, so you won’t even notice. Also remember that inflating a view does not automatically attach it to the view hierarchy unless you use the inflate (int resource, ViewGroup root, boolean attachToRoot) version of the method, passing true for attachToRoot.

Other Common Android ListView Mistakes

What other ListView mistakes have you seen? Feel free to share them in the comments below.