ListView

ListView – Part 4

In the previous article we got a ListView with a custom layout working, but warned that it was very inefficient. In this article we’ll optimise things.

let’s first try to understand precisely why the previous example is inefficient. The problem resides in the getView() method of our CustomAdapter class:

[java] @Override
public View getView( int position, View convertView, ViewGroup parent )
{
/*
* Please note that while this code works it is somewhat inefficient
* and may result in some jerky scrolling. Please read the article
* which explains this code at http://blog.stylingandroid.com/archives/623
* for further explanation and base any production code on the later,
* more efficient examples.
*/
LayoutInflater inflater = (LayoutInflater)
context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
View v = inflater.inflate( R.layout.item, parent, false );
final String item = (String) getItem( position );
TextView tv = (TextView)v.findViewById( android.R.id.text1 );
ImageView iv = (ImageView)v.findViewById( R.id.imageView );
tv.setText( item );
iv.setOnClickListener( new OnClickListener()
{
@Override
public void onClick( View v )
{
Toast.makeText( context,
String.format( “Image clicked: %s”, item ),
Toast.LENGTH_SHORT ).show();
}
} );
return v;
}
[/java]

So precisely what’s wrong here? In order to understand this we need to consider that this will need to be called for each item that is displayed in the ListView. However, the ListView implementation is quite clever and only bothers to actually draw the items that are in view. This is memory efficient, but if we do a sharp downward flick gesture on a long list, ListView is going to call getView() a lot of times in a very short space of time. Therefore we need to ensure that getView() is as efficient as possible as any inefficiencies will be compounded when ListView is handling a long scroll action.

So, what is inefficient in our code? Inflating XML layouts is expensive because it involves parsing XML and then instantiating objects to represent all of the Views in the view hierarchy. The immediate solution may appear to be constructing the view hierarchy programatically instead of using an XML layout. However, this will still result in having to instantiate a lot of java objects.

getView() has an argument that we currently dont use: convertView. ListView will try to recycle view hierarchies by passing us an item layout that we have created previously, but is no longer visible because it has been scrolled off the screen. Provided that we are using the same layout for all items, we can simply re-use the previous view hierarchy representing the layout, and just re-bind the data to this view:

[java] @Override
public View getView( int position, View convertView, ViewGroup parent )
{
View v = convertView;
if ( v == null )
{
LayoutInflater inflater = (LayoutInflater) context
.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
v = inflater.inflate( R.layout.item, parent, false );
}
final String item = (String) getItem( position );
TextView tv = (TextView) v.findViewById( android.R.id.text1 );
ImageView iv = (ImageView) v.findViewById( R.id.imageView );
tv.setText( item );
iv.setOnClickListener( new OnClickListener()
{
@Override
public void onClick( View v )
{
Toast.makeText( context,
String.format( “Image clicked: %s”, item ),
Toast.LENGTH_SHORT ).show();
}
} );
return v;
}
[/java]

So, all we’re doing here is only inflating a new view if convertView is null. This significantly reduces the amount of object deletion and instantiation that’s required, and vastly improves our scrolling performance particularly on low powered devices.

While this is a major improvement, we can still do more. Another relatively expensive method call is findViewById() which may result in traversing much of the view hierarchy in order to obtain a reference to the View in question. One excellent solution is the Holder pattern that Mark Murphy describes in his highly recommended book “The Busy Coder’s Guide to Android Development“.

As an alternative to Mark’s solution, we could instead keep a simple cache of IDs to prevent us from having to perform a findViewById() each time getView() is called:

[java] public class CustomAdapter extends BaseAdapter
{
private final Map> cache =
new HashMap>();
.
.
.
@Override
public View getView( int position, View convertView, ViewGroup parent )
{
View v = convertView;
TextView tv;
ImageView iv;
if ( v == null )
{
LayoutInflater inflater = (LayoutInflater) context
.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
v = inflater.inflate( R.layout.item, parent, false );
}
Map itemMap = cache.get( v );
if( itemMap == null )
{
itemMap = new HashMap();
tv = (TextView) v.findViewById( android.R.id.text1 );
iv = (ImageView) v.findViewById( R.id.imageView );
itemMap.put( android.R.id.text1, tv );
itemMap.put(R.id.imageView, iv );
cache.put( v, itemMap );
}
else
{
tv = (TextView)itemMap.get( android.R.id.text1 );
iv = (ImageView)itemMap.get( R.id.imageView );
}
final String item = (String) getItem( position );
tv.setText( item );
iv.setOnClickListener( new OnClickListener()
{

@Override
public void onClick( View v )
{
Toast.makeText( context,
String.format( “Image clicked: %s”, item ),
Toast.LENGTH_SHORT ).show();
}
} );
return v;
}
}
[/java]

I actually feel that Mark’s Holder pattern is both simpler to implement and more efficient than this one. I did not want to simply rip off Mark’s pattern, but wanted to provide an example of how to cache Views. So, while this pattern will work, but I would advise using Mark’s pattern instead.

The scrolling behaviour of our ListView is now much more efficient.

That concludes our series on ListView. it is by no means an exhaustive look at ListView, but should hopefully provide an introduction to this useful control.

The source code for this article can be found here.

© 2011, Mark Allison. All rights reserved.

Copyright © 2011 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. I believe the standard Holder pattern is described in Romain Guy’s Google I/0 2010 presentation “The World of ListView”: http://www.google.com/events/io/2010/sessions/world-of-listview-android.html

    I find it much simpler to use the ViewBinder mechanism that is available for SimpleAdapter and SimpleCursorAdapter. That way, you get the performance best practices for free, along with built-in binding to TextView and ImageView, and get the chance to modify or extend the binding behaviour.

    When using custom layouts, all you really care about is binding, the performance stuff is error-prone, repetitive and clutters things up. I asked Romain Guy if ViewBinder was OK to use, as it’s rarely mentioned. He answered “Yeah, we should talk about it more.”

  2. I am looking for a way to create a listview that uses only part of the screen so I can have other controls on the same screen as the listview. I assume that this is possible because of the language that I have seen used to describe them (ie. listviews **usually** take up the whole screen).

    Are partial screen listviews possible? If so, can you point me in the direction of some documentation describing how to accomplish it? FWIW, I have already scanned stackoverflow, though I have not posted a question about this yet.

    Thanks for all the great information!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.