Tuesday, 4 October 2016

Android - How to make your ListView scrolling smooth using ConverView and ViewHolder design pattern




If you are using a ListView, you will be using the getView() method in your adapter to get a view for every individual ListView element. 

If you have a poorly written code structure, you will be inflating a new view in the getView() function everytime. As a result, your scrolling will not be smooth as for every new item that comes into the view on scrolling. This is a very expensive operation and should be avoided for smooth flow especially if that view comes from an xml file and you have to inflate it because that involves reflection and tons of code is actually invoked. 

So, you shouldn't really create a new view for every item you have in the list. For instance, let's say you have 2000 contacts in your phone and you want to scroll through your list of contacts, you can't really go on creating 2000 views for every contact element. First of all, they wouldn't even fit in memory but the time it takes to create all those views( let's say 50 ms per view) will slow down the scrolling by a lot.

The solution to this is recycling views.



Here we have a simple skeleton of a ListView that can display 7 items on the screen at the same time.



When the user touches the screen and starts scrolling with her finger, one item ends up outside the screen. Something like:

ListView now needs another item to display at the bottom of the list. It will simply call the adapter and say "Give me a view" and the adapter could return a new view. But, like I said, it will be very expensive. Why do we need to create  a new view when we have one at the top (Item 1)  that we don't need to use anymore because its outside the screen. To do this, ListView has something called "Recycler" . It takes the views that are outside of the screen and sends them to the recycler. And then that item is passed to the adapter. 

When we call getView() there's this parameter called convertView by default
public View getView (int position, View convertView, ViewGroup parent) { }
convertView is actually a pointer to that view in the recycler. When we pass that used view to the adapter, the goal of the adapter is to reuse that view and simply change the text or set an image instead of creating a brand new view. So, in our case the adapter turns the old view "Item 1" into the new view "Item 8" and the ListView puts it back. All the items remain unchanged. This leads us to have very very efficient performance scrolling. So now if you are trying to fling through a list of 10 items it will be the same amount of work for the ListView if you were trying to fling through a list of 1 billion items. 


Here is what you should not do:

 public View getView(int i, View convertView, ViewGroup viewGroup) {
        View view = mInflater.inflate(mContext, R.layout.notification_item, null);
        TextView txtView = (TextView) view.findViewById(R.id.notification_text);
        txtView.setText("Item 8");
        
        return view;
 }

If you wrote code like that or if you have code like that in your application, you should go back and fix it.

You see the first line of code is to inflate an xml file. That doesn't look like a lot of work, only 2 lines of code. But again, doing that while the user is scrolling through a list of items would be very expensive. It will generate a ton of garbage which will put a lot of strain on the garbage collector. The garbage collector will then close your application and you will see the screen stutter as the system is trying to keep up with what you're doing in the adapter. 



So, instead of doing the above, do this:

 public View getView(int i, View convertView, ViewGroup viewGroup) {
        If (convertView == null) {
            convertView = mInflater.inflate(mContext, R.layout.notification_item, null);
        }
        
        TextView txtView = (TextView) convertView.findViewById(R.id.notification_text);
        txtView.setText("Item 8");
        
        return view;
 }
Just check if the convertView is null. If its null that means its probably the first layout in the ListView. Then you can actually inflate the views. It's okay to do this for the first few times till we fill the screen. But if the convertView is not null, just reuse it. You are guaranteed, if your adapter is written properly, that it will be of the right type. It will be one of the views that you created from the adapter. You can cast it to whatever view you want to find and reuse its content. There you go!




There is another cool trick called the ViewHolder.


static class ViewHolder {
  TextView text;
  TextView timestamp;
  ImageView icon;
  ProgressBar progress;
  int position;
}

The idea of a ViewHolder is to minimize the amount of work in the getView() method of the adapter. Most of the time your adapter, you will notice that even if you use convertView, will end up doing the same work all over again. If you look at the code carefully, you will find that even if I am using convertView, I am still calling findViewById() on the convertView to get the TextView that was inside my item. Do we really need to do that every time? We don't.

The idea of a ViewHolder is to hold some data that's related to the item to the view that's passed by the ListView and do the work only once. So, the way you use that is actually pretty simple. 

Take a look at the third variation of our getView() method:


 public View getView(int i, View convertView, ViewGroup viewGroup) {
        ViewHolder holder = new ViewHolder();

        If (convertView == null) {
            convertView = mInflater.inflate(mContext, R.layout.notification_item, null);
            
            holder = new ViewHolder()
            holder.text = (TextView) convertView.findViewById(R.id.notification_text);

            convertView.setTag(holder);
        }
        else {
            holder = (ViewHolder) convertView.getTag();
        }
                
        holder.text.setText("Item 8");
        
        return view;
 }

We still inflate the convertView the first time, and the first time, we also create an instance of the ViewHolder class. So, you create a new instance of ViewHolder and put inside any kind of data or reference that you need that you're gonna use every time getView() is invoked. 

So, in this case we do findViewById() the first time and we store the result of that method call into the ViewHolder and then we store that ViewHolder as a tag on a view. So, a tag is any kind of object you can set on a view. It can be used by your applications to store some random data and in this case we'll just store a ViewHolder. You are pretty much guaranteed that the framework will never use the tag automatically (except in cupcake on one particular occasion, which introduced a bug). So, if you set something on a tag, you are guaranteed that it will stay there and it will always be of the type that you just put there.

Now, here, if we get past the convertView == null statement, the first thing we need to do is get the tag on the view, which is our ViewHolder. We cast it, and then we can access it directly. 



At first it seems as if this would not make that much of a difference. We're just avoiding call to findViewById(). But upon running the above three samples on a prototype, here's what we get: 



In the "Dumb" prototype (we inflate every view), we get less than 10 frames per second on scrolling.  When you start recycling the views, you can see that the performance becomes much better and in that case we get about 27 frames per second. Finally, when we use the ViewHolder i.e. we avoid making the findViewById() call, then the performance goes up even further and will reach 37 frames per second. 


So, just by using these very simple tricks, which adds only a few lines to your adapter, you can improve your performance by three to four times very easily. And again, these are very simple tricks. So, when you have done it once, you will understand how it works and you will be able to do it everytime really easily. 





No comments:

Post a Comment