Xamarin Android - Improving GridView Recycling Performance

February 21, 2015

This is a quick performance-improvement follow-up to my previous blog post about implementing infinite scrolling in a Xamarin Android GridView

A Quick Recap

Previously, we were inflating a new instance of a view for every single item, as it was passed to us by the data adapter.

public override View GetView(int position, View convertView, ViewGroup parent)
{
  var dataItem = _mySimpleItemLoader.MySimpleItems[position];

  View itemView = convertView ?? LayoutInflater
                                .From(_context)
                                .Inflate(Resource.Layout.MyGridViewCell, parent, false);

  var tvDisplayName = itemView.FindViewById<TextView>(Resource.Id.tvDisplayName);
  var imgThumbail = itemView.FindViewById<ImageView>(Resource.Id.imgThumbnail);

  imgThumbail.SetScaleType(ImageView.ScaleType.CenterCrop);
  imgThumbail.SetPadding(8, 8, 8, 8);

  tvDisplayName.Text = dataItem.DisplayName;
  imgThumbail.SetImageResource(Resource.Drawable.Icon);

  return itemView;
}

This obviously works, and serves to demonstrate how to load an item’s details into a view so that it can be displayed on-screen. The overridden GetView method kindly takes care of recycling the views and the memory management for us.

screenshot

The silly thing about doing it this way for Every Single Item is that we know exactly what each view will look like; there’s really no need to have to locate each TextView or ImageView control, every single time. It would be ideal if we only had to look for it once, and then just reference it every time we need it.

This post will show you how to do just that, using what’s called ‘the view-holder pattern’. In a nutshell, it allows us to capture the instances of those controls on each MyGridViewCell view, and then just set their text and image source properties directly, the next time we need them.

Using the view holder method will increase the performance of your GridView (or any other view you use it with) by between 20% and 40%. When you have to keep your UI fluidly updated at 60fps, this is a massive saving! And it gives you an idea of just how much it costs your app in terms of processing time! When you only have 16 milliseconds to get your stuff ready for the next redraw pass, those precious few milliseconds you just saved can be put to better use elsewhere!

We need a view holder for each grid cell item which is (or is about to be) displayed. Not one for every item in the list. Remember, the GridView doesn’t show all the items, only the number of items which will fit on the screen. So we won’t need that many of them.

Just how many do we need? Well, it actually doesn’t matter. Or not to us, anyway. We’ll get the GetView view-generation code to make them as we need them, and then get it to just recycle the ones it’s created.

Some of you may be thinking: this seems a little complex. It’s really not. The GetView method does all of the heavy lifting for us. It’s less than 10 lines of code.

So what does a view holder look like? Well, it’s a pretty simple object, all it contains is a couple of properties corresponding to the controls we need to keep references to. In this case, we need to just keep track of a TextView and an ImageView.

public class MySimpleItemViewHolder : Java.Lang.Object
{
  public TextView DisplayName { get; set; }
  public ImageView Thumbnail { get; set; }
}

They don’t come much simpler. Note that it inherits from Java.Lang.Object. This is important, and you’ll see why in a second.

Managing the MySimpleItemViewHolder instances

The GetView method already recycles the views for us. We only need to make a couple of minor adjustments to it, in order for it to be able to make and recycle the view holders for us, too.

Briefly, this breaks down into the following operations:

  • If we don’t have a current view, then make one, and make a MySimpleItemViewHolder instance to go with it.
  • Locate the controls we need on the view (in our case TextView and ImageView).
  • Assign those controls to the corresponding properties in the MySimpleItemViewHolder instance.
  • Set the values in those properties, based on our incoming item’s DisplayName and Image source.
    • This will immediately have the effect of setting them on the controls (since they’re now pointed to by those properties).
  • Attach the MySimpleItemViewHolder instance to the current view by putting it in the Tag property of the current view.
    • This is the key step.

And if we do have a current view (i.e. currentView != null)?

  • Extract the MySimpleItemViewHolder instance from the currentView.Tag property.
  • Set the values of that instance’s DisplayName and Thumbnail image properties to the data provided by the incoming data item.
  • We’re done.

Rinse. Repeat.

Being able to place the MySimpleItemViewHolder into the Tag property is why we inherited it from Java.Lang.Object. If we’d inherited from System.Object, we’d have got an assignment error. And no, it’s not possible to cast a System.Object to (or from) a Java.Lang.Object.

So what does it look like?

public override View GetView(int position, View convertView, ViewGroup parent)
{
  var dataItem = _mySimpleItemLoader.MySimpleItems[position];

  View itemView = convertView;
  MySimpleItemViewHolder viewHolder;

  if (itemView != null)
  {
    // just use the one we made before, it came back from the recycler
    // still attached to the convertView object
    viewHolder = (MySimpleItemViewHolder)itemView.Tag;
  }
  else
  {

    viewHolder = new MySimpleItemViewHolder();
    itemView = LayoutInflater.From(_context).Inflate(Resource.Layout.MyGridViewCell, parent, false);

    // locate and init the ImageView control on the layout.
    var imgThumbail = itemView.FindViewById<ImageView>(Resource.Id.imgThumbnail);
    imgThumbail.SetScaleType(ImageView.ScaleType.CenterCrop);
    imgThumbail.SetPadding(8, 8, 8, 8);

    // assign it to the viewHolder object's ImageView property
    viewHolder.Thumbnail = imgThumbail;

    // assign the layout's TextView to viewHolder object's TextView property
    viewHolder.DisplayName = itemView.FindViewById<TextView>(Resource.Id.tvDisplayName);

    // the crucial step: attach the viewHolder to the view, so we can reuse it.
    itemView.Tag = viewHolder;
  }

  // set the values of the viewHolder properties to those 
  // provided by the dataItem
  viewHolder.DisplayName.Text = dataItem.DisplayName;
  viewHolder.Thumbnail.SetImageResource(Resource.Drawable.Icon);
  return itemView;
}

Are there any changes to the infinite scrolling aspect?

No. All the heavy lifting is being done by our view holders. Nothing else changes. Hooray! :)

Conclusion

We are able to use the GetView method’s ability to recycle the view objects to our advantage. When it recycles every view, it doesn’t (and will not) clear out the Tag object attached to each one. This gives us a safe place to store our references to the controls on each view, so we can reuse them.

Finally, the View Holder Pattern is one of Google’s best practice methods of improving scrolling-view-recycling performance. So they’re not going to clobber your Tag contents. They put it there for you to use for stuff just like this. Watch Romain Guy’s Google I/O 2009 talk on improving performance for more about view recycling and other layout and performance tips. Yes, it’s a little dated, but still relevant, and worth an hour of your time.

The full source is available in this GitHub repo. Please note: Rather than create a brand new repo for such a minor change, I’ve made these changes in the add-viewholder branch.

In my next post, I plan on demonstrating how to implement a staggered grid (like they do over at pinterest.com ) using the new Android RecyclerView and StaggeredGridLayout. Yes it’ll do infinite scrolling. And no, you won’t have to do any Java bindings.

References

Some references you may find useful:

Everything in here is released as OSS under the MIT license, so feel free to use it any way you like.


comments powered by Disqus