Logcat

for Android Developers

Handling ListViews with Multiple Row Types

| 32 Comments

When you start writing Android Apps it isn’t long before you need to use ListViews. ListViews are easy to get started with, but it’s also very easy to write inefficient lists that wreak havoc on scrolling performance. There are lot of things you can do to improve scrolling performance. You should always use convertViews to reduce creating new views. You should use the ViewHolder pattern outlined in the Android API example to reduce lookup time in layouts. If you have images in your lists the same principles outlined in my quora answer for iOS UITableViews applies to Android ListViews.

But what happens when not every item in your list view looks the same? This is often the case when you want to build a ListView that’s presenting some type of feed where some rows have images, some have videos, and some are just text and they come in no particular order. You can and should still use all the methods mentioned above. But item view reuse suddenly becomes much more complicated. You definitely don’t want to go back to not reusing the convertView, just to handle multiple row types. This is where the view type methods in the Adapter come in.

Before moving on let’s do a quick review of how ListViews and their adapters interact. If you’ve used ListViews and Adapters before before you can probably skip this paragraph. When you set an adapter on a ListView by using the setAdapater(Adapter) method, you are telling the ListView to use the adapter to tell it what to show in the list. The two key methods for the Adapter are getCount and getView. The list view calls the adapter’s getCount to know how many items exist in the list. Then only for the rows that are visible on the screen it calls getView which returns the view to show at that item.

The two additional methods android Adapters provide for managing different row types are: getItemViewType(int position) and getViewTypeCount(). The list view uses these methods create different pools of views to reuse for different types of rows.

Pools? ConvertViews? Huh?

Let’s step back for a minute and take look at how the whole convertView business works. Internally ListViews try to do a lot of work to scroll smoothly. One of the things they do is to reuse view instances when scrolling. Everytime a ListView gets a view through the getView method in your adapter, the ListView keeps that view in a pool of views that can be reused. The convertView parameter in getView is a view from this pool. If you get a non-null convertView in the getView method of your adapter, that means the ListView is telling you: “Sup dawg, I heard you want to show something here. Instead of wasting time by creating a new view, just take the one I am giving you and change its contents.”

So what about those type methods?

getViewTypeCount() tells the ListView how many of these view pools to keep. And getItemViewType(int position) tells the ListView which of these pools the view at this position belongs to. That way the ListView can give you just the right type of view as the convertView for reuse in later getView calls.

A good way to think about these pools is that ListView keeps an array of pools. The getViewTypeCount is the size of this pool array, and getItemViewType gives the index of the pool to use. What this means is that it’s important to return 0 indexed numbers in getItemViewType. It’s not a tag, it’s an index into an array.

It may help to think of a negative case to understand how this works. If you return the wrong index in getItemViewType for a particular row, then the ListView will obligingly pass you the wrong view in a convertView when you scroll. Your reuse code will then look for something that doesn’t exist and KABLAMO! Your app will crash. So it’s important to be careful about passing the right view type.

That’s a lot of words yo. Gimme some code. Show me how it really works.

Ok. Let’s use an example to explain. Let’s say we are making an app which helps you learn about animals. Let’s call this app… aah… Animals. On its home screen the app shows you an eclectic collection of Animals. Ideally you want to show an image and the name, but sometimes you don’t have an image for the animal. In that case you want to show a completely different layout in your row. Instead of the image you want to give a short description of the animal.

Animals App Screenshot

Our core model for this class is a POJO called Animal which has three fields: imageId, name, and description. The adapter is given a list of these animals to display. Since we have two types of rows our getViewTypeCount method for the adapter simply returns 2.

public int getViewTypeCount() {
    return 2;
}

The getItemViewType method returns the right index based on the data:

public int getItemViewType(int position) {

    //we have an image so we are using an ImageRow
    if (animals.get(position).getImageId() != null) return 0;

    //we don't have an image so we are using a Description Row
    else return 1;
}

Our getView method uses the same logic in getItemViewType and branch to a different way to fill up the view:

public View getView(int position, View convertView, ViewGroup parent) {
    //first get the animal from our data model
    Animal animal = animals.get(position);

    //if we have an image so we setup an the view for an image row
    if (animal.getImageId() != null) {
        ImageRowViewHolder holder;
        View view;

        //don't have a convert view so we're going to have to create a new one
        if (convertView == null) {
            ViewGroup viewGroup = (ViewGroup)LayoutInflater.from(AnimalHome.this)
                    .inflate(R.layout.image_row, null);

            //using the ViewHolder pattern to reduce lookups
            holder = new ImageRowViewHolder((ImageView)viewGroup.findViewById(R.id.image),
                        (TextView)viewGroup.findViewById(R.id.title));
            viewGroup.setTag(holder);

            view = viewGroup;
        }
        //we have a convertView so we're just going to use it's content
        else {
            //get the holder so we can set the image
            holder = (ImageRowViewHolder)convertView.getTag();

            view = convertView;
        }

        //actually set the contents based on our animal
        holder.imageView.setImageResource(animal.getImageId());
        holder.titleView.setText(animal.getName());

        return view;
    }
    //basically the same as above but for a layout with title and description
    else {
        DescriptionRowViewHolder holder;
        View view;
        if (convertView == null) {
            ViewGroup viewGroup = (ViewGroup)LayoutInflater.from(AnimalHome.this)
                    .inflate(R.layout.text_row, null);
            holder = new DescriptionRowViewHolder((TextView)viewGroup.findViewById(R.id.title),
                    (TextView)viewGroup.findViewById(R.id.description));
            viewGroup.setTag(holder);
            view = viewGroup;
        } else {
            view = convertView;
            holder = (DescriptionRowViewHolder)convertView.getTag();
        }

        holder.descriptionView.setText(animal.getDescription());
        holder.titleView.setText(animal.getName());

        return view;
    }
}

That’s all there is to it. You now know how to use the view type methods to handle different row layouts in your lists. Class is over.

Umm wait… That’s some ugly lookin’ code. Can’t we do better?

Glad you asked! There are three things particularly ugly about this code. First we are using magic numbers for the values returned by our getItemViewType and getViewTypeCount methods. Second, we are repeating the same branching pattern in two different methods, getView and getItemViewType. Third, that getView method is long. All these things together make this code brittle and hard to maintain over the long term.

So how do we deal with all these problems? We introduce the concept of a Row object. You can think of a Row as a controller for each item in your list. It’s an interface that is implemented by the two different types of Rows in our example: ImageRow and DescriptionRow. When we construct our adapter we take the list of animals it’s given and create the right Row object for each animal.

AnimalAdapter(List<Animal> animals) {
    rows = new ArrayList<Row>();//member variable

    for (Animal animal : animals) {
        //if it has an image, use an ImageRow
        if (animal.getImageId() != null) {
            rows.add(new ImageRow(LayoutInflater.from(AnimalHome.this), animal));
        } else {//otherwise use a DescriptionRow
            rows.add(new DescriptionRow(LayoutInflater.from(AnimalHome.this), animal));
        }
    }
}

So what do these Row objects actually do? Well let’s take a look at the interface definition:

public interface Row {
    public View getView(View convertView);
    public int getViewType();
}

This probably looks very familiar. That’s because these methods look almost exactly like getView and getItemViewType methods from the Adapter interface we talked about earlier. In each of these methods of the adapter we hand off the work to relevant method in the Row object itself. So when you call getView it gets the Row object for that position and asks it to return the correct view. For an ImageRow it returns a row where you have a title and an image, and for a DescriptionRow it returns a row that has a title and a description. Here’s what the getView and the getItemViewType methods on the adapter look like:

public int getItemViewType(int position) {
    return rows.get(position).getViewType();
}

public View getView(int position, View convertView, ViewGroup parent) {
    return rows.get(position).getView(convertView);
}

So what do view type methods actually return? We could just return 0 for ImageRows and 1 for DescriptionRows and when the the adapter’s getViewTypeCount method is called, return 2 and call it a day. But we wanted to avoid using magic numbers in our code so instead we use an Enum.

public enum RowType {
    IMAGE_ROW,
    DESCRIPTION_ROW
}

So getViewType for ImageRow returns RowType.IMAGE_ROW.ordinal(), and for DescriptionRow it returns RowType.DESCRIPTION_ROW.ordinal(). getViewTypeCount on our adapter simply returns RowType.values().length.

All in all our adapter looks like this:

private class AnimalAdapter extends BaseAdapter {
    final List<Row> rows;

    AnimalAdapter(List<Animal> animals) {
        rows = new ArrayList<Row>();//member variable

        for (Animal animal : animals) {
            //if it has an image, use an ImageRow
            if (animal.getImageId() != null) {
                rows.add(new ImageRow(LayoutInflater.from(AnimalHome.this), animal));
            } else {//otherwise use a DescriptionRow
                rows.add(new DescriptionRow(LayoutInflater.from(AnimalHome.this), animal));
            }
        }
    }

    @Override
    public int getViewTypeCount() {
        return RowType.values().length;
    }

    @Override
    public int getItemViewType(int position) {
        return rows.get(position).getViewType();
    }

    public int getCount() {
        return rows.size();
    }

    public Object getItem(int position) {
        return position;
    }

    public long getItemId(int position) {
        return position;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        return rows.get(position).getView(convertView);
    }
}

As you can see our Adapter code is super simple! It’s because it passed all the hard work to the Row objects which have clear ownership of what their views look like and how they behave. Obviously you’re itching to see the code for the whole app and how it all works together. So you can download the whole Animals app here.

If you want to really understand how this pattern works do the following exercise with the downloaded code. Add a third type of row: ImageDescriptionRow. If you have all three pieces of data, image, title and description for an animal, then show the title and image just like the ImageRow but also show a description below spanning the width of the row.

32 Comments

  1. Loving the site man…coming from reddit. Keep up the good work! Your posts are really helping me out.

  2. Great start guys, loving the content.

  3. Really great article. The method described in here is a really good way to make sure your Adapter’s code is modular and can be easily maintain.

    I just want to mention this method forces the developer to create a Row object for each Animal. It’s okay when you have just a few rows but it can be pretty long if you have plenty of lines (initialization is done synchronously and blocks the UI thread). Moreover, it doubles the number of objects in memory necessary to the ListView.

    Another way to handle this case is to reduce the problem to a single type of View by creating a custom View that may adapt itself depending on the given content: the only difference is the image/text on the right. In this case it’s simple but it may be pretty difficult sometimes.

    Some other tips:
    - Use a ListActivity which will remove the findViewById boilerplate and main.xml
    - main.xml has a useless LinearLayout. Remove it ;)

  4. All good points Cyril. Though there are a lot of objects created for long lists, the objects themselves are fairly lightweight so it shouldn’t be a huge issue. Maybe with REALLY long lists this could be a problem, but I’ve used it for fairly long lists without a memory problem.

    If it’s a really simple alignment changes I can see how you can reuse a single view entirely. But this tool is really very effective when you have quite different row types so you don’t have to do on the fly view mangling.

    As for the rest of the tips. All true :-) I try not to use ListActivity as a base, because very soon afterwards your UI iterates forward and you have to bring all the boilerplate back.

  5. A good post. On a relative note, a lot of developers have a hard time having a good implementation of Horizontal ListView. I see that in Pulse for Android apps, you do have horizontally scrolling lists. Is is a ListView-like control which can be mapped to an adapter, or is it a ScrollView? A post on that one, and how you do a lazy load of images only after scrolling stops, will be beneficial to many android developers.

  6. Pingback: Revue de l’actu Android Pro 15 octobre 2011 | Paris Android User Group

  7. Great thanks, Indy!

  8. Nice tutorial with clear instructions and explanation. But what would you do with a dynamic List of animals ? Let’s say you added and removed animals from the list dynamically, you would have to manage the Rows list internally the adapter in order for it work correctly right ? Or am I missing something obvious ?

    Thank you anyway for this tips.

    • The easy thing to do is to rebuild all the the Row objects and call notifyDataSetChanged(). Though I don’t remember completely: but if you set hasStableIds and keep the ids the same for the same row then calling notifyDataSetChanged is more efficient.

  9. nice post. but i am still searching for the classes ImageRow and DescriptionRow …

  10. Hi,
    I’ve been trying to integrate this adapter with SQLite database but no luck so far. Also, is there any way in which you can change the image of the animal using startActivityForResult and onActivityResult – when I try to do this via the adapter it gives me a fatal error… Thank you for sharing this!

    • If you are trying to change the image from start activity for result then you probably want to store off the result yourself in your activity, regenerate the row objects and call notifyDataSetChanged(). That’s the really simple way to do it, you can be a bit more sophisticated if this leads to performance issues, it likely won’t be bad enough.

  11. Thanks Indy…what would be the best way to work with a database instead of the array-adapter that you’ve shown in your example? Keeping everything else the same, can I just change the adapter to be a cursor adapter? I’ve actually put everything in strings so everything is an integer for me now R.strings….. or R.drawable…. etc. Your tips are much appreciated!

    • This particular way of doing things assumes everything is in memory. Though you could probably update it to use cursors. I haven’t personally worked with CursorAdapters myself so I can’t help you much more specifically.

  12. This solution still has magic numbers. If you reverse the order of the RowType enum, your solution breaks, no?

    RowType {
    IMAGE_ROW,
    DESCRIPTION_ROW
    }

    vs

    RowType {
    DESCRIPTION_ROW,
    IMAGE_ROW
    }

    • You might be confusing the first and second solutions. The first solution gives you and overview of how Android works with very basic code. The second solution provides a more general framework (After the “Ugly Code” header), if you change the enum order it wouldn’t matter in the second solution.

  13. I have an additional question: In my app I am running a background service to get the list data when this adapter is instantiated. When the service completes it calls AnimalAdapter.notifyDataSetChanged(). This has no effect however, since the AnimalAdapter constructor is where the rows are first created. So in my app this results in no update to the view once the backing data is downloaded, there are forever zero rows in the view. Any ideas for this scenario?

    • I think I got it. I’ll instead call notifyDataSetInvalidated() when the service completes, which I can override to reconstruct the rows.

      • You shouldn’t have to resort to that. There are two things you can do. One, you can simply instantiate a new Adapter and set it on the ListView after your service completes (make sure to do it on the main thread or else the system will complain!). Even better is to not make the list of rows final and have a separate method on the adapter that updates that list of rows in then call notifyDataSetChanged.

        • Nice. I’ve got it working with the second method that updates the list of rows, then notifyDataSetChanged. Was having a problem with reused convertView providing the wrong row type, so I create a new row each time :’/ (this was leading me to think there was a magic number in getRowType). My getView is now like this:

          if (convertView != null) {
          convertView.destroyDrawingCache();
          }
          // then proceed to create a new row in all cases

          But it’s working now, will return to delve deeper at a later point. Thanks again for the informative post!

  14. Pingback: Android ListView中添加不同的多种布局 - 移动端开发 - 开发者

  15. Really good post..! helps me a lot, Thanks!

  16. I just tried adding some simple notifyDatasetChanged(), i.e instantiated the List obj instead of using an anonymous class. The code fails.

  17. Thanks man. Your post is awsome! :)

  18. Why are you not using getItemViewType to determine which view type to get rather than duplicate the logic from that method ?

  19. Best tutorial I’ve read this year.

  20. First of all, thank you to offer a nice solution.

    However, I’ve tried and I get the following exception :
    “ImageRowViewHolder cannot be cast to DescriptionRowViewHolder”

    I don’t know where is the bug O_o

    • I am not sure what your code is. But it sounds like something is returning the wrong view type for certain rows in getItemViewType

Leave a Reply

Required fields are marked *.