About this post

The View Holder design pattern is one of the most important ways to increase the performance of an Android application with a ListView. However, most of the Android projects I review at Udacity entirely omit this pattern. Personally, I think that Android itself is to be blamed here. View Holder is not a complicated pattern, but it takes a bit of getting used to, and the benefits may not be that visible, so students may prefer to build their applications in the simplest way and move onto the next project.

But, once you learn how a View Holder works and how to implement one you will never go back. And it will be a lot easier for you to move onto a RecyclerView, which is a lot more powerful and flexible view comparing to a ListView. I’m planning to cover a RecyclerView in one of the upcoming posts.

About a List View

ListView is designed to tackle the problem of displaying a long or even huge list of items. Some examples may include a list of contacts, songs, your favorite coffee shops, latest transactions of your bank account or many others. The challenge here is that users can only see a small portion of these items. A user will never be able to see all of their 4579 songs at the same time, but rather a small portion of them.

 

It does not make sense to try to render the whole list at once. It would take a significant amount of time, device memory and may be unnecessary as users may be interested in the third item of the list and never scroll to the bottom.

To increase the performance of scrolling, a ListView would “recycle” its views, by taking a view which is no longer visible to the users and appending it at the bottom of the list, as shown below.

This is already a very efficient process, the only problem here is the findViewById() method, which is pretty slow and depending on the complexity of a list item it may be called multiple times per item, here is a simple example.


@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.contact_item, parent, false);
}
Contact contact = contacts.get(position);
((TextView)convertView
.findViewById(R.id.name_text_view))
.setText(contact.getName());
((TextView)convertView
.findViewById(R.id.mobile_text_view))
.setText(contact.getMobile());
((TextView)convertView
.findViewById(R.id.landline_text_view))
.setText(contact.getLandline());
return convertView;
}

As you can see in the code above, the findViewById method is called three times and this is done every single time we display a new item on the screen. However, once we implement the ViewHolder design pattern this will no longer be required.

Starter Code

I wrote a very simple application with a ListView as a starting point for this tutorial. You are welcome to fork or download the Starter Code from Github or from my blog.

You can open the project with Android Studio, compile and run it. It is a very simple application which is designed to mock a contacts list.

You can refer to the Contact.java and MockDataGenerator.java classes if you are interested in looking under the hood of the application. The first one is a Model class, which contains the first and the last name of a person as well as their contact numbers. And MockDataGenerator is used to generate a List of random contacts.

Most important, however, is the ListViewActivity.java file, so let’s have a closer look at it.


public class ListViewActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
ListView listView = (ListView) findViewById(R.id.list);
listView.setAdapter(new ContactsAdapter(this));
}
class ContactsAdapter extends ArrayAdapter<Contact> {
private List<Contact>; contacts;
public ContactsAdapter(Context context) {
super(context, –1);
this.contacts = MockDataGenerator.getMockContacts(1000);
}
@NonNull
@Override
public View getView(int position,
@Nullable View convertView,
@NonNull ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater
.from(getContext())
.inflate(R.layout.contact_item, parent, false);
}
Contact contact = contacts.get(position);
((TextView)convertView
.findViewById(R.id.name_text_view))
.setText(contact.getName());
((TextView)convertView
.findViewById(R.id.mobile_text_view))
.setText(contact.getMobile());
((TextView)convertView
.findViewById(R.id.landline_text_view))
.setText(contact.getLandline());
return convertView;
}
@Override
public int getCount() {
return this.contacts.size();
}
}
}

There are two classes in this file. Keeping different classes in the same file is not always the best practice, but I did this to make the tutorial simpler so that we don’t have to navigate between multiple different files. First class is called ListViewActivity and it extends AppCompatActivity, this is very standard. And the class implements a single method only.


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_list_view);
ListView listView = (ListView) findViewById(R.id.list);
listView.setAdapter(new ContactsAdapter(this));
}

In this method we attach the activity_list_view layout file as the main view, then we find the ListView inside of that layout and assign a new instance of the ContactsAdapater to it.

The ContactsAdapter has three methods. First, we have a constructor. It accepts an instance of Context and passes it to the super class. And then it instantiates the local variable called contacts with a list of 1000 random contacts.


public ContactsAdapter(Context context) {
super(context, –1);
this.contacts = MockDataGenerator.getMockContacts(1000);
}

Then we have a method called getCount() which returns the size of the local variable called contacts.


@Override
public int getCount() {
return this.contacts.size();
}

And lastly we have our getView() method which is responsible for inflating and recycling the views. The method is checking if the convertView parameter is null and inflates a new instance of the contact_item view if it is.

In either case, we take this view, locate the three text views inside of it and fill them up with a relevant instance of the Contact class.


@NonNull
@Override
public View getView(int position, @Nullable View convertView,
@NonNull ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.contact_item, parent, false);
}
Contact contact = contacts.get(position);
((TextView)convertView
.findViewById(R.id.name_text_view))
.setText(contact.getName());
((TextView)convertView
.findViewById(R.id.mobile_text_view))
.setText(contact.getMobile());
((TextView)convertView
.findViewById(R.id.landline_text_view))
.setText(contact.getLandline());
return convertView;
}

Adding a ViewHolder

The purpose of a view holder is to hold ._. references to the views. So, rather than trying to find a view by its identifier each time it is about to appear on the screen, we only need to do this once and then use the reference to modify the content.

We’ll start by creating a new class. This class can be located inside of the ContactsAdapter class for now. There are three text views inside of the list item, so our view holder needs to have three references. Here it is:


class ViewHolder {
private TextView nameTextView;
private TextView mobileTextView;
private TextView landlineTextView;
}

Now let’s add a constructor. The constructor needs to accept a reference to a View and instantiate all of the references to the private variables. This code will probably feel familiar.


public ViewHolder(@NonNull View view) {
this.nameTextView = (TextView)view
.findViewById(R.id.name_text_view);
this.mobileTextView = (TextView)view
.findViewById(R.id.mobile_text_view);
this.landlineTextView = (TextView)view
.findViewById(R.id.landline_text_view);
}

And this is all we need to do to implement the class itself! Now we just need to make sure to use it inside of our adapter. The only place we need to change is our getView method.

We need to create a new instance of a ViewHolder every time we inflate a list item. And then we store this reference. This is where setTag() method becomes very useful. We can assign any object to a view which we can retrieve later. Here is the modified code.


ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(getContext())
.inflate(R.layout.contact_item, parent, false);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
}

We also need to add an else statement for those situations when the convertView is already inflated. Here we just need to read the tag and cast it to the class.


} else {
viewHolder = (ViewHolder) convertView.getTag();
}

The last part is to start using the viewHolder to reference the views instead of our old code.


viewHolder.nameTextView.setText(contact.getName());
viewHolder.mobileTextView.setText(contact.getMobile());
viewHolder.landlineTextView.setText(contact.getLandline());

And we are done! You can find the final version of the code in this Github repository or download it right here.

%d bloggers like this: