HowTo: ListView, Adapter, getView and different list items’ layouts in one ListView

I was surprised that getViewTypeCount() is so rarely overrided (codesearch). If you are an expert in this – this post is not for you:-)

ListView and Adapter Basics

How it works:

  1. ListView asks adapter “give me a view” (getView) for each item of the list
  2. A new View is returned and displayed

Next question – what if we have one billion items? Create new view for each item? The answer is NO:-) Android caches views for you.

There’s a component in Android called “Recycler”. I drawed a picture based on Romain Guy presentation at Google IO ’09.

  1. If you have 1 billion items – there are only visible items in the memory + view in recycler.
  2. ListView asks for a view type1 first time (getView) x visible items. convertView is null in getView – you create new view of type1 and return it.
  3. ListView asks for a view type1 when one item1 is outside of the window and new item the same type is comming from the bottom. convertView is not null = item1. You should just set new data and return convertView back. No need to create view again.

Let’s write a simple code and put System.out to the getView:

public class MultipleItemsList extends ListActivity {
 
    private MyCustomAdapter mAdapter;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCustomAdapter();
        for (int i = 0; i < 50; i++) {
            mAdapter.addItem("item " + i);
        }
        setListAdapter(mAdapter);
    }
 
    private class MyCustomAdapter extends BaseAdapter {
 
        private ArrayList mData = new ArrayList();
        private LayoutInflater mInflater;
 
        public MyCustomAdapter() {
            mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
 
        public void addItem(final String item) {
            mData.add(item);
            notifyDataSetChanged();
        }
 
        @Override
        public int getCount() {
            return mData.size();
        }
 
        @Override
        public String getItem(int position) {
            return mData.get(position);
        }
 
        @Override
        public long getItemId(int position) {
            return position;
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            System.out.println("getView " + position + " " + convertView);
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item1, null);
                holder = new ViewHolder();
                holder.textView = (TextView)convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.textView.setText(mData.get(position));
            return convertView;
        }
 
    }
 
    public static class ViewHolder {
        public TextView textView;
    }
}

Run the program and see what happens:

getView was called 9 times. convertView is null for all visible items

02-05 13:47:32.559: INFO/System.out(947): getView 0 null
02-05 13:47:32.570: INFO/System.out(947): getView 1 null
02-05 13:47:32.589: INFO/System.out(947): getView 2 null
02-05 13:47:32.599: INFO/System.out(947): getView 3 null
02-05 13:47:32.619: INFO/System.out(947): getView 4 null
02-05 13:47:32.629: INFO/System.out(947): getView 5 null
02-05 13:47:32.708: INFO/System.out(947): getView 6 null
02-05 13:47:32.719: INFO/System.out(947): getView 7 null
02-05 13:47:32.729: INFO/System.out(947): getView 8 null

Then scroll the list slightly down (until item 10 appears):

convertView is still null because there is still no view in the recycler (border of item1 is still visible at the top:))

02-05 13:48:25.169: INFO/System.out(947): getView 9 null

Let’s scroll list a little more:

Bingo! convertView is not null: item1 goes off the screen directly to the Recycler and item11 is created based on item1.

02-05 13:48:42.879: INFO/System.out(947): getView 10 android.widget.LinearLayout@437430f8

scroll more just to check what hapens:

02-05 14:01:31.069: INFO/System.out(947): getView 11 android.widget.LinearLayout@437447d0
02-05 14:01:31.142: INFO/System.out(947): getView 12 android.widget.LinearLayout@43744ff8
02-05 14:01:31.279: INFO/System.out(947): getView 13 android.widget.LinearLayout@43743fa8
02-05 14:01:31.350: INFO/System.out(947): getView 14 android.widget.LinearLayout@43745820
02-05 14:01:31.429: INFO/System.out(947): getView 15 android.widget.LinearLayout@43746048
02-05 14:01:31.550: INFO/System.out(947): getView 16 android.widget.LinearLayout@43746870
02-05 14:01:31.669: INFO/System.out(947): getView 17 android.widget.LinearLayout@43747098
02-05 14:01:31.839: INFO/System.out(947): getView 18 android.widget.LinearLayout@437478c0
02-05 14:03:30.900: INFO/System.out(947): getView 19 android.widget.LinearLayout@43748df0
02-05 14:03:32.069: INFO/System.out(947): getView 20 android.widget.LinearLayout@437430f8

convertView is not null as we expected. After item11 goes off the screen, it view (@437430f8) comes as convertView for item 21. simple.

Different list items’ layouts

Let’s move to the “more complicated” example. How about to add separator somewhere to the list.

You should do the following:

  1. Override getViewTypeCount() – return how many different view layouts you have
  2. Override getItemViewType(int) – return correct view type id by position
  3. Create correct convertView (depending on view item type) in getView

Simple, isn’t it? Code snippet:

public class MultipleItemsList extends ListActivity {
 
    private MyCustomAdapter mAdapter;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCustomAdapter();
        for (int i = 1; i < 50; i++) {
            mAdapter.addItem("item " + i);
            if (i % 4 == 0) {
                mAdapter.addSeparatorItem("separator " + i);
            }
        }
        setListAdapter(mAdapter);
    }
 
    private class MyCustomAdapter extends BaseAdapter {
 
        private static final int TYPE_ITEM = 0;
        private static final int TYPE_SEPARATOR = 1;
        private static final int TYPE_MAX_COUNT = TYPE_SEPARATOR + 1;
 
        private ArrayList mData = new ArrayList();
        private LayoutInflater mInflater;
 
        private TreeSet mSeparatorsSet = new TreeSet();
 
        public MyCustomAdapter() {
            mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
 
        public void addItem(final String item) {
            mData.add(item);
            notifyDataSetChanged();
        }
 
        public void addSeparatorItem(final String item) {
            mData.add(item);
            // save separator position
            mSeparatorsSet.add(mData.size() - 1);
            notifyDataSetChanged();
        }
 
        @Override
        public int getItemViewType(int position) {
            return mSeparatorsSet.contains(position) ? TYPE_SEPARATOR : TYPE_ITEM;
        }
 
        @Override
        public int getViewTypeCount() {
            return TYPE_MAX_COUNT;
        }
 
        @Override
        public int getCount() {
            return mData.size();
        }
 
        @Override
        public String getItem(int position) {
            return mData.get(position);
        }
 
        @Override
        public long getItemId(int position) {
            return position;
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            int type = getItemViewType(position);
            System.out.println("getView " + position + " " + convertView + " type = " + type);
            if (convertView == null) {
                holder = new ViewHolder();
                switch (type) {
                    case TYPE_ITEM:
                        convertView = mInflater.inflate(R.layout.item1, null);
                        holder.textView = (TextView)convertView.findViewById(R.id.text);
                        break;
                    case TYPE_SEPARATOR:
                        convertView = mInflater.inflate(R.layout.item2, null);
                        holder.textView = (TextView)convertView.findViewById(R.id.textSeparator);
                        break;
                }
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.textView.setText(mData.get(position));
            return convertView;
        }
 
    }
 
    public static class ViewHolder {
        public TextView textView;
    }
}

Let’s run what we wrote. Yo will see separators after each 4-th item in the list.

In the log – nothing exceptional – all convertView is null for visible items both types.

02-05 15:19:03.080: INFO/System.out(1035): getView 0 null type = 0
02-05 15:19:03.112: INFO/System.out(1035): getView 1 null type = 0
02-05 15:19:03.130: INFO/System.out(1035): getView 2 null type = 0
02-05 15:19:03.141: INFO/System.out(1035): getView 3 null type = 0
02-05 15:19:03.160: INFO/System.out(1035): getView 4 null type = 1
02-05 15:19:03.170: INFO/System.out(1035): getView 5 null type = 0
02-05 15:19:03.180: INFO/System.out(1035): getView 6 null type = 0
02-05 15:19:03.190: INFO/System.out(1035): getView 7 null type = 0
02-05 15:19:03.210: INFO/System.out(1035): getView 8 null type = 0
02-05 15:19:03.210: INFO/System.out(1035): getView 9 null type = 1

Scroll list and see what happens:

02-05 15:19:54.160: INFO/System.out(1035): getView 10 null type = 0
02-05 15:19:57.440: INFO/System.out(1035): getView 11 android.widget.LinearLayout@43744528 type = 0
02-05 15:20:01.310: INFO/System.out(1035): getView 12 android.widget.LinearLayout@43744eb0 type = 0
02-05 15:20:01.880: INFO/System.out(1035): getView 13 android.widget.LinearLayout@437456d8 type = 0
02-05 15:20:02.869: INFO/System.out(1035): getView 14 null type = 1
02-05 15:20:06.489: INFO/System.out(1035): getView 15 android.widget.LinearLayout@43745f00 type = 0
02-05 15:20:07.749: INFO/System.out(1035): getView 16 android.widget.LinearLayout@43747170 type = 0
02-05 15:20:10.250: INFO/System.out(1035): getView 17 android.widget.LinearLayout@43747998 type = 0
02-05 15:20:11.661: INFO/System.out(1035): getView 18 android.widget.LinearLayout@437481c0 type = 0
02-05 15:20:13.180: INFO/System.out(1035): getView 19 android.widget.LinearLayout@437468a0 type = 1
02-05 15:20:16.900: INFO/System.out(1035): getView 20 android.widget.LinearLayout@437489e8 type = 0
02-05 15:20:25.690: INFO/System.out(1035): getView 21 android.widget.LinearLayout@4374a8d8 type = 0

convertView is null for separator view type until first separator is visible. When it goes off the screen – view also comes to the Recycler and convertView comes to play.

MultipleItemsList.zip – source code

enjoy)
//DL

  • Share/Bookmark

Comments (11)

BMarch 18th, 2010 at 9:07 pm

You rock! Thanks for the tutorial.

androidstarMay 14th, 2010 at 3:26 pm

great tutorial

bactismeMay 26th, 2010 at 7:42 am

The separator item is still clickable, right ? did you success to make it act as a true separator like in a PreferenceScreen ?

villain_dmMay 26th, 2010 at 10:13 am

override isEnabled(int position) to make it not clickable

Dryfus RandalMay 27th, 2010 at 2:26 am

Thanks for the tutorial, I need a slightly different version. I so hope you will help me out. Day three here on this and my head is flat in the front from pounding it on my desk. So, here goes…

I need the simplist way to achieve a multicolumn ListView where I have the linearlayout file with two TextViews. The first is file name, the second is file creation date, time. The second should be disabled so as not to to be able to click on it.

Now, I cannot for the life of me figure out how to get my product to consume (without error) the following:

At best I get the “no [type of file string here] to show” message back, thanks to the way the xml for the activity UI was setup.

So, how can I achieve this. I’m using ArrayList now. One of the important lines is as follows:

ArrayAdapter fileList = new ArrayAdapter(this, R.layout.row, item);

Thanks for any help the blog author can offer, or anyone else that sees this and takes a minute to dig me out of my coding hole.

Dryfus RandalMay 27th, 2010 at 2:50 am

OMG! I just changed one line of code, transposing the row.xml arg with my other xml (that is the row’s structure)!

ArrayAdapter fileList = new ArrayAdapter(this, R.layout.row, R.id.rowsetup, item);

So, all is well. Thanks again for anyone about to help or would have!

MattMay 28th, 2010 at 5:32 am

How do you go about add an item click listener to this. 7 days and I can’t figured it out.
I appreciate any help.

nah0yJune 3rd, 2010 at 11:40 pm

Thanks for the code :D

Great explications, and great tutorial !

AmitJuly 2nd, 2010 at 2:32 pm

nice tutorial…
keep it up.. :)

sarveshJuly 21st, 2010 at 4:55 pm

It was really wonderful tutorial. Can you help me I have an issue regarding this….

I was implementing listview completely through code(no inflation through .xml). My list view has 6 visible items, so when my application launches in android 2.0, getView should be called 6 times. But when i try to print the position, it is giving as
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
0
1
2
3
4
5
0
1
2
3
4
5
6
7
0
1
2
3
4
5
6
7
I dont know why, you can check my code:

public View getView(int position, View convertView, ViewGroup parent)
{
System.out.println(position);
ListItemViewHolder holder;
if (convertView == null)
{
System.out.println(“new”);
holder = new ListItemViewHolder();
holder.listImageView = getImageView();
holder.textView = getTitle(position);
holder.listIconView = getAppLogoImage(holder.textView, position);
RelativeLayout listRelativeLayout = new RelativeLayout(mContext);
listRelativeLayout.addView(holder.listImageView,0);
listRelativeLayout.addView(holder.textView,1);
listRelativeLayout.addView(holder.listIconView,2);
convertView = (View) listRelativeLayout;
convertView.setTag(holder);
}
else
{
holder = (ListItemViewHolder) convertView.getTag();
}
holder.textView.setText(mAddApplications.get(position).getAppName());
if (position == 0 && getViewFlag == false)
{
lastChild = convertView;
currentChild = convertView;
holder.textView.setTextColor(Color.BLACK);
holder.listImageView.setBackgroundResource(R.drawable.widget_selected);
getViewFlag = true;
}
else
{
holder.textView.setTextColor(Color.WHITE);
holder.listImageView.setBackgroundResource(R.drawable.widget_default);
}
holder.listIconView.setImageDrawable(mAddApplications.get(position)
.getAppIconDrawable());
return convertView;
}

Note: It is printing
0
1
2
3
4
5
as expected in android 1.5

Is there any difference in implementation in android 2.0?

andreagostoSeptember 7th, 2010 at 5:44 pm

nice post, thanks!

Leave a comment

Your comment