13 12 / 2011

Improving the Android ListView

I wanted to take a few minutes to talk about my experience using the ListView in Android.  For my non-developer friends, the ListView is the widget that organizes information in an item by item structure that scrolls when necessary.  It’s used to implement your contacts list, among many other views in Android.

I’ll often refer to the ListView as the most important widget in the Android UI toolbox.  Maybe that’s me being overly dramatic, when compared to objects like the Button, TextView, or layouts like LinearLayout or RelativeLayout.  I’d argue my case by pointing to the countless examples where a ListView’s usage is critical to the mobile app it’s being used in.  Think about these few examples, Google+/Twitter/Facebook, PicPlz, Gmail, Settings, Text Messages, Contacts, RSS readers, and Music.  The notion of a scrollable list of information, implemented using the ListView, is at the core of the user experience of all of these apps and I’m positive you can rattle off a dozen yourself.  The nature of a touch screen provides the need for information to be displayed this way, so it’s no surprise this one widget finds itself being so critical to such a huge number of apps.

The ListView is so core to so many apps, you’d think it would be a dream to work with as a developer.  I’ll caveat the rest of my thoughts by saying I’m no expert, and there very well may be better ways to approach some of the scenarios I’ll bring up.  But if there are, Google hasn’t been very helpful in my discovery of them. :)

In simple scenarios, the ListView is a very simple and efficient widget to implement in Android.  Take the Settings app, where each item simply displays an icon and short text describing the general settings available for your device.  Each item is simple and the XML layout for each is likely very compressed which leads to a truly optimal experience for the ListView.  The scroll is fluid in Settings.  You can flick the screen and everything behaves as you’d expect.

Looking at a more complex example, such as the Google+ app for Android, you’ll see a far more complex item structure.  Each post in your stream is an item in the ListView.  Each post contains a profile image for the person who posted it, their name, how long ago it was posted, it’s visibility, the text of the post, possibly a media element such as a photo or video, possibly they’ve shared someone else’s post that duplicate everything above, and finally how many +1’s and comments the post has gotten.  That’s a lot of data, especially for a single ListView item.  Starting at the top of your stream, if you quickly flick the screen you’ll notice images are loaded dynamically, videos begin to cue up as you fly by them, and as these things happen the UI will pause even for just a moment as it goes by each item of any complexity.

I’ve spent the last few days very focused on one task; performing any kind of optimization I can for the heavy ListViews I’m using in my next app.  I’ve learned a few things along the way, some have been pretty obvious to me since starting my exploration of Android more than a year ago:

1. Caching images used in ListView items with SoftReference or WeakReference is useless.

2. Reusing the view passed to getView() in your adapter is extremely important.

3. Using findViewById() in getView() is expensive, therefore the ViewHolder pattern is highly recommended.

4. Getting a reference to the LayoutInflater is often unnecessarily done in getView(), you should get a reference to it in your adapter’s constructor.

5. Using a dynamic image in each item negates some of the optimizations I’ve outlined above.

Explanations

1. Caching images used in ListView items with SoftReference or WeakReference is useless.

Here’s an example of lazy loading images, where you’re requesting each image to be downloaded on a background thread and stored in-memory for future usage.  It sounds fantastic, until you start seeing OutOfMemory exceptions after loading a number of images.  The next step will be using SoftReference so that your objects can be garbage collected when necessary, but still afford you some form of simple caching.  Now it sounds perfect, until you do some testing and realize that your objects are being released almost instantly as you scroll past them due to a bug in the Dalvik VM.

What this boils down to is needing to roll your own cache, for something that seems to be pretty common for ListView implementations.  I would love to see a subclass of ListView added to the Support Library that implements a cache for images and improves the drawing of the list as it scrolls.

2. Reusing the view passed to getView() in your adapter is extremely important.

In the getView() method, which is called for every list item, you’re given a View.  That’s a reference to the view already being shown with the idea being you can reuse it with new data, instead of inflating a new list item every time.  This isn’t always possible, but it’s a must when you can do it.

3. Using findViewById() in getView() is expensive, therefore the ViewHolder pattern is highly recommended.

This is something I learned recently and it made sense when I first saw it.  The ViewHolder pattern is a static class that has a variable for each UI widget in your list item XML layout.  In the getView() of your adapter, you call findViewById() for each widget you need to set a value for and store the reference in the ViewHolder instance.  When you’re done, you get the tag of the view to the ViewHolder.  So on future calls, you can simply get the tag and have direct hooks to each of your widgets without searching your item structure using findViewById() every single time.  Again, some scenarios break it’s usefulness, but use it when it’s possible.

4. Getting a reference to the LayoutInflater is often unnecessarily done in getView(), you should get a reference to it in your adapter’s constructor.

Kind of an obvious thing, but another I had never seen in code examples until recently.  Most of the time you’ll see adapter implementations where a LayoutInflator is being retrieved from the system every time you need to inflate a new list item.  You can move this call into the constructor of your adapter, and use the instance in getView() without constantly refreshing it.

5. Using a dynamic image in each item negates some of the optimizations I’ve outlined above.

And now for the killer.  When you’re loading in a dynamic image for each list item, such as a profile image for the Google+ app, many of these optimizations aren’t possible.  I’ve been unable to find a way to reuse the view being passed into getView(), because when using the ListView you’ll see incorrect images loading for items because the sequencing gets messed up.  As a side effect of not being able to reuse the view passed into getView(), I’m unable to effectively take advantage of the ViewHolder pattern because I have to inflate the new list item every time anyway.  It seems like doing any sort of call to load an image dynamically inside of getView() really causes poor scrolling performance in the ListView.

More Tips

A few other suggestions I’ve seen recently but haven’t investigated yet:

1. Simplify your XML structure for the list item.

2. Use RelativeLayout instead of LinearLayout in your list item.

3. On your ListView, set DrawingCacheEnabled to true to improve performance on scrolling.

While not all of these are possible due to my use case, I’m curious to dig deeper into #2 to see if I can find anything tangible.  Same for #3, I’ve enabled it and it appears to perform better but it’s hard to quantify.

Conclusion

The ListView is a critical widget for any Android developer, and I’d argue it’s the most critical.  It’s been at the core of every app I’ve ever built and I don’t see that changing any time soon.  The ListView is powerful in it’s current state and there are little nuggets of information out there to improve it’s efficiency and power.  I’d like to see the performance of the ListView improve and better support for some of the use cases that have shown since the ListView was originally developed.  It’s a blind assumption, but I imagine the way we’re using ListView today is more involved than the developers originally expected and it seems to be putting a strain on what it can handle efficiently.  Hopefully the above tips I’ve found throughout my searches and exploration will help someone, as they’ve appeared to help the performance of my next app’s complex ListView.

Permalink 1 note