Android Listview Examples and Guide
Android ListViews have a bit of a learning curve, and they are used extensively in many apps including the recently released Backspin Music Player. I've put together a few tips and gotchas to keep in mind when working with them. In this article I assume you understand how to set a ListView's adapter, but if you are brand new to using ListViews, learning that would be a good place to start.
View Recycling
One of the most important things to understand about ListViews is how they try to reuse their individual item views. Inflating views for items that aren't on screen is wasteful. ListViews get around this by asking you to only inflate enough views for the visible items. Then, when a view is scrolled off the end of the visible area, that view can be reused for the newly visible item at the other end of the list. Adapters automatically take care of the recycling for you, so you just need to check if your list item's getView
method is receiving a recycled view or not. You only need to inflate a new view if the passed in convertView is null.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = View.inflate(getActivity(), R.layout.item, parent);
}
...
return convertView;
}
If you inflate a new view every time getView
is called, instead of reusing the views, you are going to have a memory leak. The ListView obviously has references to its item views, and it doesn't do anything to associate specific views with the data backing them. This means that as you scroll up and down your list, you will keep inflating more and more views until either the list itself can be garbage collected or you run out of memory and crash.
The View Holder Pattern
If your list feels sluggish to scroll, it probably means that your adapter's getView
is taking too long to run. The View Holder pattern can help alleviate this. The basic idea behind the View Holder pattern is to cache the results of findViewById
, and it can be an easy way to improve your ListView's performance significantly. findViewById
uses a simple recursive search of the view hierarchy to find the view you are looking for. Generally speaking, the performance cost here isn't something to worry about, but the problems begin when your list item's layout starts getting complicated and users fling through your ListView quickly. This can quickly result in hundreds of calls to findViewById
for each frame along with a sluggish feeling ListView.
To use the ViewHolder pattern, create a class whose members are the various view elements you need to search for inside of getView
.
public class ViewHolder {
TextView text;
...
}
Now we populate our ViewHolder when we first inflate the item's layout. We can use the View's tag field to give it a reference to our ViewHolder object. Because we are using the view recycling I mentioned earlier, we can retrieve our ViewHolder and reuse it when we are given a previously inflated item view.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = View.inflate(getActivity(), R.layout.item, parent);
viewHolder = new ViewHolder();
viewHolder.text = convertView.findViewById(R.id.text_id);
...
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder)convertView.getTag();
}
viewHolder.text.setText("Hello");
...
return convertView;
}
Multiple View Layouts
In the Backspin music player, we often had to have multiple, different item layouts in a single list. For example, under an artist, there may be a row for an album, rows for all the songs in that album, and then another album row. Android's Adapter makes handling these cases trivial. Override the adapter's getViewTypeCount()
to return the number of unique layouts the view can show. Then Override getItemViewType()
to map a position to the index of its view layout. Android does all of the heavy lifting, and our getView
will automatically be given a recycled view of the correct layout.
@Override
public int getViewTypeCount() {
return 2;
}
private static final int ITEM1 = 0;
private static final int ITEM2 = 1;
@Override
public int getItemViewType(int position) {
if (isItem1(position)) {
return ITEM1;
} else {
return ITEM2;
}
}
ListView.invalidateViews() vs Adapter.notifyDatasetChanged()
So you have made some changes to the data backing a ListView, how do you now get the ListView to draw those changes on screen? If you look at the Android documentation, the two obvious candidates are ListView.invalidateViews()
and Adapter.notifyDatasetChanged()
. The trick here is that invalidateViews can't observe changes to the Adapter's data and does nothing but redraw the views that are currently on screen. This means that if you added an item to the Adapter, it will not appear and if you removed an item, it will not disappear. However, if you want to change the color of the text of your views without touching the data behind them, ListView.invalidateViews()
may be all that you need. On the other hand, notifyDatasetChagned()
tells the Observer watching your adapter to look for changes in the list's data. After that, the views are redrawn taking the new data into account. The short answer is that notifyDatasetChanged
does everything invalidateViews
does and more, and almost always does everything you are looking for.
Async Tasks
Your ListView has no hope of performing well if you are doing things like accessing the disk or a database on the main thread inside your adapter's getView
. Every time you scroll the list, you are going to block all of the UI logic while you wait for your data to load. The best solution is to load your data outside of your view logic before you try to display it. If that is not possible, perhaps in the case of thumbnails, an alternative must be found. A solution is using AsyncTasks. AsyncTasks are a simple way to perform slow operations off of the main thread and then display the results on the UI thread.
@Override
public View getView(int position, View convertView, ViewGroup parent) {
...
new AsyncTask<Void, Void, Bitmap>() {
@Override
public Bitmap doInBackground(Void... params) {
return BitmapFactory.decodeFile(...);
}
@Override
public void onPostExecute(Bitmap bitmap) {
imageView.setBitmap(bitmap);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return convertView;
}
Of course multi-threading is never this easy. First, this is complicated by the way the adapter recycles list items. If you start an AsyncTask to modify a view and then that view gets recycled before the Async task ends, the AsnycTask is going to keep running. When the AsyncTask finishes, it is still going to modify the view even though that view is being used somewhere else entirely. Another problem is that when scrolling, a new AsyncTask is going to be fired for each view that becomes visible. This means that if you scroll down and then up, you are potentially going to have two AsyncTasks running for the same item. All of this can be overcome by keeping track of which AsyncTasks have already been started and making use of AsyncTask.cancel
.
public class ViewHolder {
ImageView imageView;
AsyncTask asyncTask;
...
}
Calls to cancel will let your AsyncTask continue to run, but will prevent onPostExecute
from being run once it completes. Note that because getView
and onPostExecute
run on the UI thread, we don't need to worry about a race condition here. Now getView
can look something like this:
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
...
if (viewHolder.asyncTask != null) {
viewHolder.asyncTask.cancel();
}
viewHolder.asyncTask = new AsyncTask<Void, Void, Bitmap>() {
@Override
public Bitmap doInBackground(Void... params) {
return BitmapFactory.decodeFile(...);
}
@Override
public void onPostExecute(Bitmap bitmap) {
imageView.setBitmap(bitmap);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return convertView;
}
Another concern is that you can only have 128 AsyncTasks running at once (with 10 more queued up), so if someone flings through your list quickly and your tasks are too slow, you may run into a RejectedExecutionException
. As I mentioned, tasks that have been canceled still run to completion and count against this 138 AsyncTask cutoff until they are done. A solution here might be to only start a task if the list is scrolling slowly enough that your tasks will have time to finish loading before they are scrolled off the page. You can look into overriding the AbsListView.ScrollListener
so that you don't start tasks during quick flings of the ListView. You should also look into caching the results of your AsyncTasks so you don't have to keep queueing new tasks to load the same data. Something like Android's LRUCache
might be useful here.
Clearly AsyncTasks can be a solution, but they make everything more complicated. If you are going to use them in ListViews, you need to be mindful of the many ways they can cause problems.
Conclusion
Android Listviews are one of the most commonly used and misused Android UI elements. Properly implementing them can significantly improve your app's performance and stability. Do you have other tricks or tips related to Listviews? If so, please drop us a message and we will include it in this article. Also, if you are looking for help on your upcoming Android app project, make sure you check out our Android development services!