Removing logic from RecyclerView/ListView Adapters

Below is a common use case for RecyclerView where it shows a list of items.

For example, the backend would return a list of data that include shipping type, location where it’s shipped from, and shipped date time; perhaps more information. Suppose the text color of each shipping type is an important business logic. This is a simplified example.

example

So what do you mean by “stop doing work”?

Inside ViewHolder class called from RecyclerViewAdapter.onBindViewHolder():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void bind(Shipping item) { // Shipping is a web service response model
    Context context = itemView.getContext();
 
    shippingTypeText.setText(item.getShipppingType());
 
    if (ShippingType.STANDARD.equals(item.getShipppingType())) {
        shippingTypeText.setTextColor(ContextCompat.getColor(context , R.color.shipping_type_standard));
    } else if (ShippingType.PRIORITY.equals(item.getShipppingType())) {
        shippingTypeText.setTextColor(ContextCompat.getColor(context , R.color.shipping_type_priority));
    } else if (ShippingType.SPECIAL.equals(item.getShipppingType())) {
         shippingTypeText.setTextColor(ContextCompat.getColor(context , R.color.shipping_type_special));
    } else {
         shippingTypeText.setTextColor(ContextCompat.getColor(context , R.color.shipping_type_default));
    }
 
    shippingFromText.setText(item.getShippingFrom().getCity() + ", " + item.getShippingFrom().getStateAbbr());
 
    // Formatting a string date time into display format
    dateTimeText.setText(DateTimeUtil.format(item.getShippedDateTime()));
}

In this example, date time parsing/formatting and non-UI/business logic are happening in ViewHolder or inside the Adapter. As user scrolls, bind() is being called many many times as RecyclerView/ListView recycles its items. Notice that the result of the work done here will not change.

What’s wrong?
  • Prevent possibility of scroll lag if you overdo work
  • Could increase complexity by mixing view and non-UI logic
    • I would definitely not want to see business logic in a ViewHolder.
  • Extra usage of memory
    • Avoid instantiating objects.
  • You can’t write unit test since this code is buried inside Adapter/ViewHolder.




The biggest problem

I have seen real code where ListView Adapter holds web service response model. All of the formatting and business/domain logic were directly happening at ViewHolder level.

For example, when there is a change to the web service response, it may impact unnecessary spots in the code base making it chaotic to support the change. Or suppose the information/data at ViewHolder level is no longer sufficient to meet the new UI requirement. You would need to refactor it to move that logic to an upper layer.

People cut corners under pressure. You can imagine how it can go wrong over time.

Solution: Do the work before

ViewHolder class after refactoring:

1
2
3
4
5
6
void bind(ShippingItem item) {
    shippingTypeText.setText(item.getShipppingType());
    shippingTypeText.setTextColor(ContextCompat.getColor(itemView.getContext(), item.getShippingTypeColor()));
    shippingFromText.setText(item.getShippingFrom());
    dateTimeText.setText(item.getShippedDateTime());
}

UI model:

1
2
3
4
5
6
7
8
9
public class ShippingItem {
 
    private String shipppingType;
    private @ColorRes int shippingTypeColor;
    private String shippedDateTime;
    private String shippingFrom;
 
    // Getters & setters....
}

Create a UI model. Use it only to display a row. Get the value and set to Views. Perhaps decide to whether show or hide a View based on value’s existence. Don’t worry about anything else inside ViewHolder. It’s less coupling and single responsibility.

Some benefits
  • Encourages separation of concern
  • ViewHolder becomes light and easy to understand
  • Lets us focus on UI/view code in ViewHolder

Leave a Comment