Tips for ListView: View recycling, use the ViewHolder....

All android devs use extensively ListView in their apps.
I think many of us, also myself of course, have written at least once a very bad Adapter.

I think that the first time there was talked about performance and optimization of listview, was at I/O 2009.
You can find this document.
At Google IO 2010,the subject was taken up again.Here you can find the document.

There are 2 key points:
  • View recycling
  • View Holder pattern
I would say that many of Android examples that I see do the first item properly. It's pretty simple, use the convertView on simple adapters like ArrayAdapter or use the newView on more involved adapters like CursorAdapter. But I must say that the ViewHolder pattern is not so common.

It is very important to understand what it is, and why we should use view recycicling and ViewHolder pattern.

Let's take a small example. It is a BAD ADAPTER. You should never do it!!
/**
 * Bad Adapter.
 * 
 * DON'T USE IT !!!!!
 * 
 */
public class MyBadAdapter extends ArrayAdapter {

    .......

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

        Log.d(TAG, "position="+position);
        LayoutInflater mInflater = mContext.getLayoutInflater();
        View item = mInflater.inflate(R.layout.list_item, null);

        MyObj data = getItem(position);

       ((TextView) item.findViewById(R.id.text1)).setText(data.text1);
       ((TextView) item.findViewById(R.id.text2)).setText(data.text2);
       ((TextView) item.findViewById(R.id.longtext)).setText(data.longText);
       return item;
    }
}
Debug the code.
Every time ListView needs to show a new row on screen, it will call the getView() method from its adapter.

If you try to scroll down your list you can see it (!!):

ListView try to inflate a lot of views when scrolling so it is very very important to make your adapter’s getView() as lightweight as possible.

Now. Layout inflations are very expensive.
ListView is optimized and recycles non visible items (called ScrapViews). We can see the code here and image below.
This means that when convertView argument is not-null, you should simply use it instead of inftating a new row layout.
We can improve our adapter by recycling view.
public class RecycleAdapter extends ArrayAdapter {
   
    public View getView(int position, View convertView, ViewGroup parent) {

       Log.d(TAG, "position=" + position);

       if (convertView == null) {
            convertView = mInflater.inflate(R.layout.list_item, parent, false);
       }
       
       MyObj data = getItem(position);
      ((TextView) convertView.findViewById(R.id.text1)).setText(data.text1);
      ((TextView) convertView.findViewById(R.id.text2)).setText(data.text2);
      ((TextView) convertView.findViewById(R.id.longtext)).setText(data.longText);
      
      return convertView;
   }

}



It is better, but there is another problem.
The problem is findViewById().
We'll try to understand why.
The findViewById() Android API method requires an algorithm to search your view hierarchy every time you call it by scanning recursively the entire structure looking for the ID of your view. Here you can find the code.


It is very expensive, because you will call this method for each view in rows, for each row.

The View Holder pattern reduces the number of findViewById() calls in the adapter’s getView().
It stores each of the component views inside the tag field of the Layout, so you can immediately access them without the need to look them up repeatedly.
You can read about it in official doc.
You store it as a tag in the row’s view after inflating it.
public class ViewHolderAdapter extends ArrayAdapter {
    
    static class ViewHolder {
         TextView text1;
         TextView text2;
         TextView longtext;
    }

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

         Log.d(TAG, "position=" + position);

         ViewHolder holder;

         if (convertView == null) {
               convertView = mInflater.inflate(R.layout.list_item, parent, false);
               holder = new ViewHolder();
               holder.text1 = (TextView) convertView.findViewById(R.id.text1);
               holder.text2 = (TextView) convertView.findViewById(R.id.text2);
               holder.longtext = (TextView) convertView.findViewById(R.id.longtext);
               convertView.setTag(holder);
         } else {
               holder = (ViewHolder) convertView.getTag();
         }
 
         MyObj data = getItem(position);
         holder.text1.setText(data.text1);
         holder.text2.setText(data.text2);
         holder.longtext.setText(data.longText);
         
         return convertView;
   }
}

One last tip.
Try to use this layout for your ListView:

<!--xml version="1.0" encoding="utf-8"?-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="fill_parent"
    android:layout_width="fill_parent" 
    android:orientation="vertical" >

    <ListView android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:drawSelectorOnTop="false" />

</LinearLayout>
It is the worst thing possible with a ListView by giving it a height=wrap_content

You can see here, why it is a bad solution.
Here the debug output:
06-27 13:15:46.962: D/MyBadAdapter(25084): position=0
06-27 13:15:47.002: D/MyBadAdapter(25084): position=1
06-27 13:15:47.032: D/MyBadAdapter(25084): position=2
06-27 13:15:47.062: D/MyBadAdapter(25084): position=3
06-27 13:15:47.082: D/MyBadAdapter(25084): position=4
06-27 13:15:47.112: D/MyBadAdapter(25084): position=5
06-27 13:15:47.132: D/MyBadAdapter(25084): position=6
06-27 13:15:47.162: D/MyBadAdapter(25084): position=7
06-27 13:15:47.192: D/MyBadAdapter(25084): position=8
06-27 13:15:47.212: D/MyBadAdapter(25084): position=9
06-27 13:15:47.242: D/MyBadAdapter(25084): position=10
06-27 13:15:47.272: D/MyBadAdapter(25084): position=11
06-27 13:15:47.292: D/MyBadAdapter(25084): position=12
06-27 13:15:47.352: D/MyBadAdapter(25084): position=0
06-27 13:15:47.382: D/MyBadAdapter(25084): position=1
06-27 13:15:47.412: D/MyBadAdapter(25084): position=2
06-27 13:15:47.442: D/MyBadAdapter(25084): position=3
06-27 13:15:47.472: D/MyBadAdapter(25084): position=4
06-27 13:15:47.502: D/MyBadAdapter(25084): position=5
06-27 13:15:47.532: D/MyBadAdapter(25084): position=6
06-27 13:15:47.562: D/MyBadAdapter(25084): position=7
06-27 13:15:47.602: D/MyBadAdapter(25084): position=8
06-27 13:15:47.632: D/MyBadAdapter(25084): position=9
06-27 13:15:47.662: D/MyBadAdapter(25084): position=10
06-27 13:15:47.702: D/MyBadAdapter(25084): position=11
06-27 13:15:47.732: D/MyBadAdapter(25084): position=12
06-27 13:15:47.992: D/MyBadAdapter(25084): position=0
06-27 13:15:48.032: D/MyBadAdapter(25084): position=1
06-27 13:15:48.072: D/MyBadAdapter(25084): position=2
06-27 13:15:48.112: D/MyBadAdapter(25084): position=3
06-27 13:15:48.162: D/MyBadAdapter(25084): position=4
06-27 13:15:48.202: D/MyBadAdapter(25084): position=5
06-27 13:15:48.242: D/MyBadAdapter(25084): position=6
06-27 13:15:48.282: D/MyBadAdapter(25084): position=7
06-27 13:15:48.312: D/MyBadAdapter(25084): position=8
06-27 13:15:48.342: D/MyBadAdapter(25084): position=9
06-27 13:15:48.372: D/MyBadAdapter(25084): position=10
06-27 13:15:48.392: D/MyBadAdapter(25084): position=11
06-27 13:15:48.462: D/MyBadAdapter(25084): position=12

The getView() method will be called many times !!!
android:layout_height="wrap_content" forces ListView to measure a few children out of the adapter at layout time, to know how big it should be.
Never use it !!


You can get code from GitHub:

Comments

Popular posts from this blog

AntiPattern: freezing a UI with Broadcast Receiver

How to centralize the support libraries dependencies in gradle

NotificationListenerService and kitkat