Your ViewHolders Could Be Doing So Much More

Jon F Hancock
Jon F Hancock
Published in
6 min readNov 29, 2016

--

Note from the author: This blog post uses the word “dumb” in a way that I regret because it demonstrates ignorance and ableism on my part. I have changed the title, but not the content of this post because I feel that erasing mistakes is less valuable than apologizing for them. So let me do that here. I apologize for my ableism in this post. I regret it, and I have worked to remove ableist language from my vocabulary.

I can’t decide if it is a holdover from ListView, or just a paradigm that people can’t shift away from, but I see RecyclerView ViewHolders that do absolutely nothing other than hold references to some views. These are dumb ViewHolders. They know nothing about the data they display. They know nothing about the ways users can interact with the data. They know nothing about how to communicate with code around them. But they can do so much more! They can be Not Dumb!

Let’s learn by example! -- Somebody probably

The best way to work through this paradigm shift is by example. Let’s start with a dumb ViewHolder and an Adapter that has too much work to do. Then we’ll refactor it step by step.

First of all, let’s look at our data model. It’s a simple, immutable class with a handful of fields. Some are for display, and some are for building Uris. There’s only one potentially new concept here. The era field is a String, but it may only be “BC” or “AD”. That’s exactly what StringDef does for us.

public class ExcellentAdventure {
...
}

For the full data model class, visit this gist.

Now our DumbViewHolder is really simple. Probably too simple. Its only purpose is to make all of those findViewById calls not be in our adapter.

public class DumbViewHolder extends RecyclerView.ViewHolder {
TextView textTitle;
TextView textLocation;
TextView textDate;
ImageView mapIcon;

public DumbViewHolder(View itemView) {
super(itemView);
textTitle = (TextView) itemView.findViewById(R.id.text_title);
textLocation = (TextView) itemView.findViewById(R.id.text_location);
textDate = (TextView) itemView.findViewById(R.id.text_date);
mapIcon = (ImageView) itemView.findViewById(R.id.map_icon);

}
}

Now our poor OverworkedAdapter has a whole lot to do. It must bind the fields to each individual view in the ViewHolder, then handle the click events on two separate views. Then it even has to start activities!

Something new in the Adapter below is DiffUtil. (Full code in the gist) If you’ve been using notifyDatasetChanged to let your Adapter know that it’s underlying data has been refreshed, you should stop now. Use DiffUtil to compare the old data to the new data and let it call notifyItemRangeInserted, notifyItemRangeRemoved, and notifyItemRangeChanged for you. One very important thing to note though is that although it looks like an asynchronous callback, it is not. In this example DiffUtil processes and notifies on the main thread, and it will block until it is done. If your list is very large, the calculation can take a long time. In that case, you should call calculateDiff on a background thread. Then when the calculation is complete, and returns a DiffResult, you can update your adapter data, and dispatch changes on the main thread.

public class OverworkedAdapter extends RecyclerView.Adapter {

...

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DumbViewHolder vh = (DumbViewHolder) holder;
final ExcellentAdventure item = items.get(position);
vh.textTitle.setText(item.getTitle());
vh.textLocation.setText(item.getLocationName());
vh.textDate.setText(getFormattedDate(item));
vh.textTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchWikipedia(item);
}
});
vh.mapIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchGoogleMaps(item);
}
});
}

private String getFormattedDate(ExcellentAdventure item) {
String date = item.getYear() + " " + item.getEra();
return date;
}

...
}

All right! So how can we improve the cohesion of these classes? First of all, let’s make the ViewHolder a little smarter. Let’s inform it about the kind of data it’s holding. We don’t want to use the constructor to pass in an ExcellentAdventure to the ViewHolder because the adapter can create ViewHolders before it needs to use them. Creation and binding are separate steps. So let’s add a field to our ViewHolder, and a setter to fill it.

public class DumbViewHolder extends RecyclerView.ViewHolder {
TextView textTitle;
TextView textLocation;
TextView textDate;
ImageView mapIcon;

ExcellentAdventure item;

...

public void setItem(ExcellentAdventure item) {
this.item = item;
}
}

Woohoo! Now let’s rip all that setText code out of the adapter, and move it to our new method!

public class DumbViewHolder extends RecyclerView.ViewHolder {    ...    public void setItem(ExcellentAdventure item) {
this.item = item;
textTitle.setText(item.getTitle());
textLocation.setText(item.getLocationName());
textDate.setText(getFormattedDate(item));
}
private String getFormattedDate(ExcellentAdventure item) {
String date = item.getYear() + " " + item.getEra();
return date;
}

}

That makes our Adapter a little simpler, but not a whole lot.

public class OverworkedAdapter extends RecyclerView.Adapter {
....
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DumbViewHolder vh = (DumbViewHolder) holder;
final ExcellentAdventure item = items.get(position);
vh.setItem(item);
vh.textTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchWikipedia(item);
}
});
vh.mapIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchGoogleMaps(item);
}
});
}
...
}

That’s still a lot of code that is very specific to this view type. We can do better. Let’s move those onClickListeners to the ViewHolder too! Now this is especially nice. We can set the onClickListeners once in the constructor rather than over and over again at bind time. We can save a few microseconds for each bind and get us just a little closer to perfectly jank-free scrolling.

It may seem counterintuitive to set click listeners that will use the item’s data before we even have an item. “Wouldn’t the item be null?!” you might exclaim. Only if your users can click faster than your adapter can bind (unlikely). You see, the onClick methods are not evaluated until the user clicks the view. By that time, setItem will have been called, and we will have data to work with.

public class DumbViewHolder extends RecyclerView.ViewHolder {
...

public DumbViewHolder(View itemView) {
super(itemView);
...
textTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchWikipedia(item);
}
});
mapIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
launchGoogleMaps(item);
}
});
}

...

}

Now we’ll see a massive improvement in our Adapter. Just look how concise this Adapter is now! Our whole Adapter in just 70 lines. Complete with animations when items are deleted, inserted, or changed.

public class OverworkedAdapter extends RecyclerView.Adapter {
...

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
DumbViewHolder vh = (DumbViewHolder) holder;
vh.setItem(items.get(position));
}

...
}

Now this is great already. We can improve things even more by having our ViewHolder define an Interface that the Activity or Fragment can implement, and the Adapter can pass to the ViewHolder. Now we can let the Activity decide what to do with the information instead of always having the ViewHolder or Adapter worry about it. Perhaps in our tablet layout, we have a map fragment in a separate pane, and we want to update that instead of launching google maps directly when the map icon is clicked. It really doesn’t matter because our ViewHolder just doesn’t care anymore.

public class DumbViewHolder extends RecyclerView.ViewHolder {
...
ExcellentAdventureListener listener;

public interface ExcellentAdventureListener{
void onMapClicked(ExcellentAdventure item);
void onTitleClicked(ExcellentAdventure item);
}

public DumbViewHolder(View itemView, final ExcellentAdventureListener listener) {
super(itemView);
this.listener = listener;
...
textTitle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onTitleClicked(item);
}
});
mapIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onMapClicked(item);
}
});
}

...

}

Then our Adapter just needs an updated Constructor so it can pass the listener through.

public class OverworkedAdapter extends RecyclerView.Adapter {
...
private ExcellentAdventureListener adventureListener;

public OverworkedAdapter(LayoutInflater inflater,
ExcellentAdventureListener adventureListener) {
...
this
.adventureListener = adventureListener;
}

...

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = inflater.inflate(R.layout.item_excellent_adventure, parent, false);
return new DumbViewHolder(v,adventureListener);
}
...
}

Now this is a Smart ViewHolder. It’s also a Super Chill Adapter now. It does what it needs to do, and delegates stuff that it shouldn’t know or care about to the appropriate places.

This works wonderfully with a simple case like this, but it really shines when you have a very complex data set with different types that have different views, and different user interactions. Each type can have a separate ViewHolder, and each ViewHolder knows how to display its data, and how to pass click events up the chain. Furthermore your Adapter can focus on what it does best: adapting list items to view holders. It doesn’t have to bind each individual field. It can pass the whole item to the respective ViewHolder, and call it a day.

Oh and one more thing. Another really awesome thing you can do now is reuse your ViewHolders. Perhaps you have a few different Activities that display the same kinds of data, but in different formats. This ViewHolder can pull double duty as long as it has the same view ids to work with.

Have a look at the finished code here.

--

--