ListView and ViewHolder
Enhance ListView's performance to reduce strain from the UI thread
One of the main concerns an Android developer should have is reducing strain from the main thread, also known as the UI thread.
ListView is one of the most common views in Android and it is important to know how to write a fast adapter for it. One of the ways to enahnce your ListView performance is by using a ViewHolder pattern.
Assume we have an object named Profile which describes some basic information of a person. It has an image, a name and a short bio.
public class Profile {
/**
* Name of person.
*/
private String mName;
/**
* Bio.
*/
private String mBio;
/**
* Profile image resource.
*/
private int mProfileImageResource;
public Profile(String name, String bio, int profileImageResource) {
mName = name;
mBio = bio;
mProfileImageResource = profileImageResource;
}
/* "Get" methods... */
}
We would also want to create a ListView of Profiles, in which every row looks like this:
and can be described in XML ( R.layout.list_item
) like this:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_profile_image"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/tv_bio"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/iv_profile_image"
android:padding="@dimen/padding_profile_text"/>
<TextView
android:id="@+id/tv_bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/iv_profile_image"
android:layout_toEndOf="@+id/iv_profile_image"
android:paddingLeft="@dimen/padding_profile_text"/>
</RelativeLayout>
To start, we should write an adapter ( ArrayAdapter<Profile> for the sake of an example ) which should look something like this:
public class MyArrayAdapter extends ArrayAdapter<Profile> {
private Context mContext;
/**
* A layout inflater to create Views from XML.
*/
private LayoutInflater mInflater;
/**
* Row View resource.
*/
private int mResource;
/**
* List of profiles.
*/
private ArrayList<Profile> mProfiles;
public MyArrayAdapter(Context context, int resource, ArrayList<Profile> list) {
super(context, resource, list);
mContext = context;
mResource = resource;
mProfiles = list;
mInflater = LayoutInflater.from(mContext);
}
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/**
* Missing implementation.
*/
}
}
getView()
is the main method we should focus on here, as it's the method which gets called by the system every time a new View needs to be inflated, updated and returned - to display the data ( a Profile ) at the specified position in the data set ( Profiles list in our case ).
Let's consider the following getView()
:
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/**
* 1. Inflating layout from resource.
*/
final View rowView = mInflater.inflate(mResource, parent, false);
/**
* 2. Finding views by id.
*/
TextView name = (TextView) rowView.findViewById(R.id.tv_name);
TextView bio = (TextView) rowView.findViewById(R.id.tv_bio);
ImageView img = (ImageView) rowView.findViewById(R.id.iv_profile_image);
Profile profile = mProfiles.get(position);
/**
* Setting information.
*/
name.setText(profile.getName());
bio.setText(profile.getBio());
img.setImageResource(profile.getProfileImageResource());
return rowView;
}
Very slow!
As the ListView is being scrolled and getView()
is called, two tasks happen:
R.layout.list_item
is inflated into a new View.- A series of
findViewById()
calls are executed, one call for each inner View in our layout.
Getting called frequently, these two tasks are very performance-expensive, especially if R.layout.list_item
is large and more complex.
1. Using ListView's recycling mechanism
To take care of the first task and avoid inflating the same View over and over again, we can take advantage of ListView's inner recycling mechanism. ListView works with a RecycleBin
object ; a data set used to store unused views that should be reused during the next layout to avoid creating new ones.
Sounds great! How do we know when a View is being recycled ? By the value of convertView
argument we get in getView(int position, View convertView, ViewGroup parent)
's second parameter.
When convertView
is not null, the system actually tells us : "There you go, an already inflated View which is currently not visible to the user and which you can use to update its contents according to your data set and position, instead of inflating a new one."
Therefore, we can update our getView()
code to effectively use this recycling behavior:
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
/**
* 1. Inflating layout from resource.
*/
if (convertView == null) {
convertView = mInflater.inflate(mResource, parent, false);
}
/**
* 2. Finding views by id.
*/
TextView name = (TextView) convertView.findViewById(R.id.tv_name);
TextView bio = (TextView) convertView.findViewById(R.id.tv_bio);
ImageView img = (ImageView) convertView.findViewById(R.id.iv_profile_image);
Profile profile = mProfiles.get(position);
/**
* Setting information.
*/
name.setText(profile.getName());
bio.setText(profile.getBio());
img.setImageResource(profile.getProfileImageResource());
return convertView;
}
Faster! But not enough.
2. The ViewHolder pattern
To take care of the second task, and reduce the amount of findViewById()
calls, a ViewHolder pattern takes place.
As you can quite easily guess, A ViewHolder is an object whose only purpose is to hold references and give access to our layout's inner Views.
The ViewHolder will hold the returning Views from findViewById()
calls and will be kept as a tag object inside convertView
for later retrieval and usage.
Our ViewHolder looks like this:
private static class ViewHolder {
TextView name;
TextView bio;
ImageView img;
}
Let's use it to update getView()
and enhance our ListView performance even further:
@NonNull
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder vh;
/**
* 1. Inflating layout from resource.
*/
if (convertView == null) {
convertView = mInflater.inflate(mResource, parent, false);
vh = new ViewHolder();
/**
* 2. Finding views by id.
*/
vh.name = (TextView) convertView.findViewById(R.id.tv_name);
vh.bio = (TextView) convertView.findViewById(R.id.tv_bio);
vh.img = (ImageView) convertView.findViewById(R.id.iv_profile_image);
/**
* Keep the ViewHolder as a tag for later retrieval and usage.
*/
convertView.setTag(vh);
} else {
/**
* Grab the already stored ViewHolder to save findViewById() calls.
*/
vh = (ViewHolder) convertView.getTag();
}
Profile profile = mProfiles.get(position);
/**
* Setting information.
* This time - using our already set ViewHolder.
*/
vh.name.setText(profile.getName());
vh.bio.setText(profile.getBio());
vh.img.setImageResource(profile.getProfileImageResource());
return convertView;
}
private static class ViewHolder {
TextView name;
TextView bio;
ImageView img;
}
Extremely faster!
As you can see, when convertView
is null, we do not only inflate a new View but also create a new ViewHolder. This ViewHolder holds references to Views returned from the calls to findViewById()
. We use the function View#setTag(Object)
to save the populated ViewHolder inside convertView
, so that when we get a not null convertView
in one of the next calls to getView()
, we'll be able to retrieve it back via View#getTag()
, skip the calls to findViewById()
and save time !