What type of Android developer are you?

T.Surkis
AndroidPub
Published in
9 min readJul 10, 2017
We can spot a few different ViewHolders in this real life RecyclerView grid. Credits belong to: https://www.instagram.com/cinamon357

At one point or another, we have all written an application that displays data in a list form. Whether you were around when ListView was the way to show information in a list or started developing when RecyclerView became the standard for that, you are familiar with RecyclerView and the ViewHolder design pattern.

At this point, you belong to roughly one of two groups when it comes to writing an implementation of an Adapter and ViewHolders.

ViewHolder view class members access from the adapter

Through public access of your ViewHolder view class members, the adapter puts the relevant data inside them during the onBindViewHolder method.

class Adapter {  ...  public void onBindViewHolder(...) {  
viewHolder.textOne.setText(...);
viewHolder.myImageView.setImageResource(...);
}
... ViewHolder {
public TextView textOne;
public ImageView myImageView;
... }
}

ViewHolder view class members are private

The ViewHolder view class members are private. Therefore, for the Adapter to pass the relevant data to the ViewHolder, a unique method is created inside the ViewHolder to receive it. Then the method puts the data inside the private view class members.

class Adapter {  ...  public void onBindViewHolder(...) {
viewHolder.setDataInViews(...);
}
... ViewHolder extends RecyclerView.ViewHolder {
private TextView textOne;
private ImageView myImageView;

...

public void setDataInViews(...) {
textOne.setText(...);
myImageView.setImageResource(...);
}
}
}

Why does it even matter?

If you are of the first type, your code ignores some of the most important Object Oriented Programming principles. If you are of the second category, tag along to find out how to enhance your code.

How does the first way of writing an Adapter ignore some of the most important Object Oriented Programming principles?

Encapsulation

Encapsulation is bundling of data with the methods that operate on that data. It is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties’ direct access to them.

It is uncommon to come across a list of items of a different kind. The elements are represented as different type ViewHolders. For each type of a ViewHolder exists different data. Using the first method will result in something of the sort:

void onBindViewHolder(...) {
switch(viewHolder.getItemViewType()){
case HEADER: {
((HeaderViewHolder) viewHolder).headerTitle.setText(...);
break;
}
case POST_ITEM: {
((RegularViewHolder) viewHolder).postTitle.setText(...);
((RegularViewHolder) viewHolder).postImage.setImage(...);
break;
}
case Ad: {
...
}
...
}
}

Adding an item might seem simple, just add some code into the onCreateViewHolder and onBindViewHolder methods. Consequently, more lines of code are added to our Adapter — the more ViewHolders we have, the less usable it becomes. Having even three simple ViewHolders will inflate the Adapter.

Why don’t we just create a separate class for each ViewHolder? Creating a class for each ViewHolder will leave us with classes that have public member fields. At first, it doesn’t sound too much of a bad idea. However, when we look deeper, it breaks an Object Oriented Programming principle. Where a POJO with immutable public fields will be okay, since the data is immutable anyways, a ViewHolder with public class members will pose a problem. Why? The ViewHolder fields are mutable, and that ignores one of the pillars of Object Oriented Programming: encapsulation. Furthermore, a deeper look reveals to us that the original code is also ignoring the encapsulation principle in the same manner.

Inheritance

A class is based on another class, using the same implementation or specifying a new implementation to maintain the same behavior.

Of course, having ViewHolders with identical class members is not uncommon as well. Using inheritance might seem the optimal solution to save us from two identical code pieces.

onBindViewHolder(...) {
switch(viewHolder.getItemViewType()){
...
case AD: {
((AdViewHolder) viewHolder).adView.setAd(...)
break;
}
case SPECIAL_AD: {
((SpecialAdViewHolder) ViewHolder).adView.setAd(...)
break;
}
...
}
}
AdViewHolder extends RecyclerView.ViewHolder {
public AdView adView;
...
public AdViewHolder(View itemView) {
super(itemView)
adView = (AdView) itemView.findViewById(R.id.ad);
...
}
}
SpecialAdViewHolder extends AdViewHolder {
...
public SpecialAdViewHolder(View itemView) {
super(itemView);
}
}

In the example above AdView is a custom view that could exist in a project.

However, when accessing the Adapter ViewHolder views in the onBindViewHolder, we are accessing each view of a ViewHolder separately since each one needs a different data in most cases. The full power of inheritance only saves us a few lines of instantiation code, missing the mark by a long shot — yet another pillar of Object Oriented crumbles.
Given that, using the first way to write an Adapter with inheritance seems like a waste.

Abstraction

Involves the facility to define objects that represent abstract “actors” that can perform work, report on and change their state, and “communicate” with other objects in the system.

Correspondingly, not using inheritance forces us to communicate with each ViewHolder as it is since there is no general base upon which all are structured. Summing it up, each ViewHolder is unique. Therefore, the abstraction principle of Object Oriented cannot be applied in this situation.

Polymorphism

The provision of a single interface to entities of different types. A polymorphic type is one whose operations can also be applied to values of some other type, or types.

This principle just gathers dust in your tool belt since it has become unusable by ignoring three of the Object Oriented Programming principles mentioned above.

OOP is broken, so what?

Breaking Object Oriented Programming will result in a less extensible, maintainable and modular code. Your code will become your enemy.

How bad can it be?

Extensibility

In reality, most of the adapters written in the first scheme look way messier. Frequently, more steps are involved in the onBindViewHolder method and often than not, some views depend on other views.

Additionally, the add \ update \ remove methods of your Adapter might include more logic dependent on your previous ViewHolders.

Moreover, adding even a single view to a ViewHolder will require us to create the field, instantiate it and then update its data in the Adapter onBindViewHolder method. Changing the view to a different type will compel us to change its type all across the previously mentioned areas. Having a reliable refactoring tool in Android Studio is helpful, but changing a field in one class consequently requiring us to change it in another, is bad practice and bug prone.

Before you know it, extending the class is nearly impossible without breaking something.

Maintainability

Expanding on the previous section, if you have just a few simple ViewHolders then maintaining their code will not pose a problem. However, your Adapter probably has a lot of code in it, requiring you to sometimes ‘minimize’ methods in Android Studio to get to the important parts. Additionally, having a bug in a ViewHolder will most likely result in fixing both the ViewHolder and the Adapter since they are so tightly coupled. And, if that ViewHolder inherits from another ViewHolder or is inherited from, the class dependent on it needs to be updated as well.

Even if your Adapter has a single ViewHolder — don’t you find it weird that for a single ViewHolder to be fixed or updated, you have to go through the entire Adapter code?

All of a sudden, maintaining your adapter is nearly impossible.

Modularity

What if we have a ViewHolder we want to use in another Adapter? Seeing as it is so tightly coupled to the Adapter, we will need to copy paste it.
I hope you cringed reading that sentence just as I did writing it. Why? Two existing copies of our ViewHolder will double the maintenance — fixing or modifying one code piece will force us to update the second.

What about removing a ViewHolder? We will have to search for the ViewHolder usage areas across the Adapter and delete them. Yes, Android Studio has a useful refactoring tool. However, if one or more copies of our ViewHolder exist, we might completely forget about them.

That small code-change-one-hour task you had to make in your adapter turns into a day’s or more worth job.

How to fix it?

If ignoring the important Object Oriented principles led us here, shouldn’t we utilize them for our benefit?

Encapsulation Utilization

Following this principle, the member classes of a ViewHolder become private. Now that the fields are private, we can create a unique method that receives the data from the Adapter. Internally the data is allocated across the ViewHolder fields. As a result, the ViewHolder is responsible for its member classes — Seems logical, doesn’t it?

onBindViewHolder(...) {
switch(viewHolder.getItemViewType()) {
case HEADER:{
((HeaderViewHolder) viewHolder).setHeaderData(...);
break;
}
case AD: {
((AdViewHolder) viewHolder).setAdData(...);
break;
}
case SPECIAL_AD: {
((SpecialAdViewHolder) viewHolder).setSpecialAdData(...);
break;
}
 ...
}
}
HeaderViewHolder extends RecyclerView.ViewHolder {
...
setHeaderData(...) {
headerTitle.setText(...);
}
}
AdViewHolder extends RecyclerView.ViewHolder {
...
setAdData(...) {
adView.setAd(...);
}
}
SpecialAdViewHolder extends RecyclerView.ViewHolder {
...
setSpecialAdData(...)}
specialAdView.setAd(...);
}
}

Inheritance Utilization

Looking at the example code above, the AdViewHolder and SpecialAdViewHolder have the same view used. This time, the full power of inheritance can be utilized.

onBindViewHolder(...) {
switch(viewHolder.getItemViewType()) {
case HEADER:{
((HeaderViewHolder) viewHolder).setHeaderData(...);
break;
}
case AD:
case SPECIAL_AD: {
((AdViewHolder) viewHolder).setAdData(...);
break;
}
...
}
}
HeaderViewHolder extends RecyclerView.ViewHolder {
...
setHeaderData(...) {
headerTitle.setText(...);
}
}
AdViewHolder extends RecyclerView.ViewHolder {
...
setAdData(...) {
adView.setAd(...);
}
}
SpecialAdViewHolder extends AdViewHolder {
...
}

The solution looks great, but what about the rest of the ViewHolders? This solution does not cure us yet of the horrors of the switch-case code.

Abstraction and Polymorphism Utilization

Studying the new approach regarding ViewHolders, it is easily noticeable that all the ViewHolders have a single method of data entry. That allows us to create a general base for all the ViewHolders, treating them as the same one.

onBindViewHolder(...) {  
int viewType = viewHolder.getItemViewType();
Object data = getData(viewType, position);
viewHolder.setDataInViews(data);
}
public Object getData(int viewType, int position) {
Object data = null;
switch (viewType) {
case HEADER: {
...
data = ...
}
case AD:
case SPECIAL_AD: {
data = ...
}
return data;
}
HeaderViewHolder extends BaseDataViewHolder<String> {
...
setHeaderData(String title) {
headerTitle.setText(title);
}
}
AdViewHolder extends BaseDataViewHolder<Ad> {
...
setAdData(Ad ad) {
adView.setAd(ad);
}
}
SpecialAdViewHolder extends AdViewHolder {
...
}

Uncertain about generics? Check out my in-depth article:

It might seem as if we are done, but in reality, we moved the problem elsewhere since the switch-case still exist. Of course, we are far from over — we need a better solution.

MVP to the rescue

If unfamiliar or uncertain about MVP this is a great article explaining it:

Why MVP? In this architecture, the view is “dumb” and asks the presenter what to do. Therefore, the presenter will hold getter methods — one getter method per data.

class Presenter {  ...  String getHeaderTitle() {
...
}

Ad getAd(int position) {
...
}

PostItem getPostItem(int position) {
...
}
}

The Adapter will pass the presenter instance to each ViewHolder in the onBindViewHolder method.

class Adapter {

...
onBindViewHolder(...) {
viewHolder.setDataInViews(presenter);
}
}

And each ViewHolder will ask a corresponding getter method for data.

HeaderViewHolder extends BaseViewHolder {
...
setDataInViews(Presenter presenter) {
String title = presenter.getHeaderTitle();
headerTitle.setText(title);
}
}
AdViewHolder extends BaseViewHolder {
...
setDataInViews(Presenter presenter) {
Ad ad = presenter.getAd(getAdapterPosition());
getAdadView.setAd(ad);
}
}
SpecialAdViewHolder extends AdViewHolder {
...
}
PostItemViewHolder extends BaseViewHolder {
...
setDataInViews(Presenter presenter) {
PostItem post = presenter.getPostItem(getAdapterPosition());
...
}
}

Thus, all the above mentioned Object Oriented Programming principles apply. If you look close enough, you will realize that we achieved the second method of writing an Adapter — welcome to the other side!

How is the data handled?

Instead of allocating the data when the ViewHolder requests it, it is mapped before the adapter initialization.

The Presenter analyzes the data and constructs two data collections:

  • A map of views. Each item is an integer that represents the view type; the index is its location in the list.
  • A map of data locations. Each item is an integer that represents the location of the data in the data collection; the index is its location in the list.

When the getItemViewType method of the adapter is called, the map of view types is queried to retrieve the view type in the requested position.
Correspondingly, in the onBindViewHolder when data is requested the mapped UI index points to the relevant data collection.

I have created an example project on GitHub. The project represents all the examples in the article and ranges from the most modular to the least modular solution.

Conclusion

I reached each conclusion laid out in this article over the course of my Android development career, and it is a representation of the various Adapter ways I have tried over the years.
The code written in my GitHub repository has three various code examples in achieving all that I have written above and should be seen as a blueprint more than a single way to write an Adapter.
The beauty of Android is its versatility, and there is no right way for everything but a few good ways.
If you have suggestions or questions, feel free to write them in the comment section below.

Click the ❤ button to let me know if you enjoyed the article and follow me on Medium and Twitter.

--

--

T.Surkis
AndroidPub

A Flutter developer by day, a technology enthusiast by night.