Handling ListView Recycle on Android

android android-dev listview view-recycling

March 12 2013 - 23:59


When you want to create a ListView and face the problem of some duplicated items, and don't know how to fix. what turns out is, android recycles views when scrolling. If there is already a view object that just needs different data then that view will be passed in with the convertView parameter. If this happens the code simply returns the same view again. This explains why you see duplicated items on your ListView.

But the question is, is view recycling necessary/worth?

There are many reasons for recycling views:

  • Object creation is relatively expensive. Every additional object that is created needs to be dealt with by the garbage collection system, and at least temporarily increases your memory footprint;
  • This is more important for more complex views, but inflating and laying out the view objects can be expensive. Most often, you are only making minor changes to the view in getView that won't affect the layout (e.g, setting text) so you might be able to avoid the layout overhead;
  • Remember that android is designed to be run in a resource constrained environment;
  • Finally, its already done for you, and it certainly doesn't hurt anything, so why not use it;

Below is an example of how view recycling works. It's basically reusing the last view to be the first again. That's why we must make sure the recycling is made correctly.

View Recycling example

So how to handle this? I'm gonna show you a simple example.

We must create the xml file that will represent the row itself (i mean, the row of the listview). So it will be called list_item.xml.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/listitem_bg" >

    <ImageView 
        android:id="@+id/imageViewContactIcon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/ic_contact_picture" />

    <TextView 
        android:id="@+id/textViewContactName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/imageViewContactIcon"
        android:textStyle="bold"
        android:textSize="16dp"
        android:textColor="#444444"
        android:layout_marginTop="5dp"
        android:layout_marginLeft="5dp" />

    <TextView android:id="@+id/textViewContactNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textViewContactName"
        android:layout_toRightOf="@+id/imageViewContactIcon"
        android:textSize="12dp"
        android:textColor="#444444"
        android:layout_marginTop="5dp"
        android:layout_marginLeft="5dp" />

</RelativeLayout>

Now, let me show you how the adapter must be made so the view recycle is handled correctly.

public class MyArrayAdapter extends ArrayAdapter<Contact>
{
    Context mContext;
    List<Contact> mContactsList;

public MyArrayAdapter(Context context, int textViewResourceId, List<Contact> contacts) 
{
    super(context, textViewResourceId);
    mContext = context;
    this.mContactsList = contacts;
}

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    View row = convertView;
        ViewHolder holder = new ViewHolder();

    if(row == null)
    {
        row = mInflater.inflate(R.layout.list_item, parent, false);
            // initialize the elements
        holder.contactIcon = (ImageView) row.findViewById(R.id.imageViewContactIcon);
        holder.contactName = (TextView) row.findViewById(R.id.textViewContactName);
        holder.contactNumber = (TextView) row.findViewById(R.id.textViewContactNumber);

            row.setTag(holder);
    }
        else
        {
            row = (ViewHolder)row.getTag();
        }

    // get the contact from the list
    Contact contact = mContactsList.get(pos);

    if(contact != null)
    {
        // get the contact picture based on the contact id 
            Bitmap contactPictureBitmap = mContactController.getContactPicture(contact.getM_id());

            holder.contactIcon.setImageBitmap(contactPictureBitmap); // set the retrieved contact picture

            holder.contactName.setText(contact.getmDisplayName() == null ? contact.getM_id().toString() : contact.getmDisplayName());
            holder.contactNumber.setText(phoneNumber.getmPhoneNumberValue());
    }

        return row;
    }
}

public class ViewHolder
{
    ImageView contactIcon;
    TextView contactName;
    TextView contactNumber;
}

So, explaining why the magic happens after the if-else statement in the getView method. as I explained before:

If there is already a view object that just needs different data then that view will be passed in with the convertView parameter. If this happens the code simply returns the same view again.

So, in getView if the row parameter is null we create it and set the holder pattern. if the row parameter is not null, we get the view from the holder, we will set the data to the holder attributes and return the view.

This way you make sure the views will contain the correct data, and you wont have any duplicated items.

I hope this post helps handle this issue, and if you want to learn more about how view recycling works, check this question on stackoverflow or watch the romain guy presentation at google IO ’09.

Please share and comment with doubts, opinions, etc. thanks!