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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
} else { | |
viewHolder = (ViewHolder) convertView.getTag(); | |
} |
The last part is to start using the viewHolder
to reference the views instead of our old code.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Hi, please make the color of code text darker. its very difficult to read
Hi Jaz,
Thank you very much for the feedback. I have moved all of the code snippets to GitHub, so hopefully it is easier to read now! 🙂
If we are creating a new instance of viewholder everytime getView() get called then we are calling findViewById() the same amount of times without viewholder. Where is the point of better performance by using less findViewById()? I just dont get it.
Hi Wisam, we are only creating a new instance of ViewHolder when
convertView
isnull
. That happens during the initial load of the screen but not when we scroll the list up and down. I hope that answers your question.