Delegate Adapters: Building Heterogeneous RecyclerView Adapter

An elegant way to eliminate the adapter hell and manage multiple view types even with nested RecyclerViews

Sultan Seidalin
10 min readFeb 27, 2020

--

Introduction

RecyclerView is pretty common widget that has replaced ListView in Android Development long ago and it has brought us a lot of amazing features that are still being developed. I’m not going to dive into basics of ListView and RecyclerView, so let’s assume that we all know what these widgets are created for. Most of the time, we use RecyclerView to display some kind of uniform item view, such as simple text with an icon or something a bit more complex. However, sooner or later you might face a situation where you will be required to implement a RecyclerView with multiple view types.

Hi, My name is Sultan Seidalin. I am an Android and iOS developer from Kazakhstan. In this article, I want to share how to implement a RecyclerView Adapter which will be able to handle multiple item view types easily.

Motivation

Once, I received a new task from the product owner and the UI/UX designer. They showed me the design of the new screen which was going to be a part of the upcoming feature.

The main requirements for this screen:

  • It’s a screen which shows your booking and all the information regarding your flight.
  • It has a list of cards where each card represents a small part of your flight’s information (e.g passenger information, refund information, etc.).
  • Each card has its own functionality (they may have clickable elements inside).
  • The order of these cards can be changed according to some future requirements.
  • These cards can be shown or hidden according to some conditions.
  • Other cards with different information and functionality might be added in the future.

So, the main property of our implementation should be the ease of modification. We should be able to modify the list of items without needing to rewrite or extensively modify existing code.

A great amount of inspiration was taken from “Joe’s great adapter hell escape” by Hannes Dorfmann

Conventional way

Whenever there is a need to implement multiple view types developers use constants which are used to define each view type. While it’s fine and dandy for couple of items, it quickly becomes a mess when you try to implement five or more view types. What if you need to implement different layouts as shown in the image? Let’s have a look at the conventional way of writing RecyclerView Adapters with multiple view types:

If you continue writing your code in the way shown above, you are in a big trouble. You will end up with a lot of ‘if/else’ or ‘when’ statements which will quickly create a mess in your adapter. Moreover, think about adding one more layout to this adapter. You will have to add one more statement to onCreateViewHolder() and onBindViewHolder() methods, which breaks the ‘O’ part in SOLID principles that states “Objects or entities should be open for extension, but closed for modification”.

Anytime you find yourself writing code of the form “if the object is of type T1, then do something, but if it’s of type T2, then do something else,” slap yourself.

- Danny Preussler

To solve this problem you need to implement a generic solution that works for any view type without any hazel of ‘if/else’ blocks.

An elegant and more efficient way

In order to implement such a solution we want to breakdown the task into the smallest possible pieces and solve each of them separately. What if we could have multiple adapters where each of them handles its own view type and one master adapter that binds them all together?

That’s what we are going to implement (well, not in the darkness): multiple tiny adapters that can handle their own view type and one composite adapter which will manage all those tiny adapters. I will assume you are familiar with DiffUtil, as I am not going to cover it in this article. We will use the term delegate adapter instead of tiny adapter in this article from now on.

Step 1 — Model

First of all, we have to create an interface for all the model classes that will be used in our adapters. Conventionally, you would have created a model class which would be used by ViewHolder to bind to its views. In our case, all the model classes have to implement DelegateAdapterItem interface in order to be a part of our composition.

Here, we created three functions which are used by DiffUtil to calculate the difference between items in the list. Additionally, we created a marker interface to be able to create payloads for each item (in order to change only desired part of the ViewHolder without recreating it entirely). At the end, we have a payload value (Payloadble.None) which is assigned by default. If we ever need custom payload, we will have to override this method.

Step 2 — DiffUtil Callback

Implement a generic DiffUtil class which works for our DelegateAdapterItem model class as shown below.

Here, we use DiffUtil to check if two items are the same or not and if their contents have changed. If the contents change, then appropriate payload is calculated.

Step 3 — Abstract Adapter Class

Now that we have our model set up, let’s move on to the delegate adapter. First, we have to create an abstract class which will be extended by our delegate adapters.

Basically, it’s a small adapter which uses model class to tell your master adapter that it handles such type of models. You will see how it is used in the following steps, so for now, think of it as itemViewType, which is used to distinguish ViewHolders. Functions createViewHolder() and bindViewHolder() are common, they will be used accordingly. Other functions, such as onViewRecycled(), onViewAttachedToWindow() and onViewDetachedFromWindow() are left open because not every adapter should implement them.

Step 4 — Delegate adapter & Model Implementation

Let’s create an instance of our delegate adapter with a model class. Our adapter will extend the abstract class shown above and implement corresponding methods.

Here is our model class which will be used in one of our delegate adapters that will only handle flight information. It looks quite huge, but only because I’ve tried to cover all the possible cases.

In our case, bookingId is used as a return value of id() function to show that two models are the same if their bookingIds are equal.

Next, for the content() function we created a separate inner class which is used to determine if contents of two items are different. We could have used one of the properties as return value as well, but I tried to cover the complex case, where you would need to check multiple fields or implement complex logic to compare two items.

In the end, a sealed class which implements Payloadable interface is created to have complex payloads. In our case, if departure or arrival time changes, ViewHolder will only change corresponding views without refreshing itself from scratch. Then we use payload() function to create appropriate payload object, which is then passed to adapter to update ViewHolder.

Let’s move on to our adapter:

Functions createViewHolder() and bindViewHolder() must be familiar to you, they act the same way as in any other RecyclerView adapter. Inside bindViewHolder() I used ‘when’ statement to cover multiple cases. If DiffUtil sends us callback that only departure time or arrival time has changed, then bindDepartureTime() or bindArrivalTime() functions will be called. In all other cases, we will update the entire ViewHolder as we always do.

Step 5 — Composite Adapter Implementation

Now that we have all our smallest parts implemented (models and adapters) let’s create our composite adapter which binds everything together. Generally, composite adapter is no different from any other ListAdapter. But instead of working with a list as usual, it works with a list of adapters and whenever a method is called from ListAdapter it passes the responsibilities to the delegate adapter. Let’s have a look at the source code:

It extends ListAdapter and uses DelegateAdapterItem as a model. Our DiffUtil Callback from Step 2 is passed to ListAdapter’s constructor. Composite adapter has its own constructor that waits for a SparseArray (which is basically a map with key of type Int, and an value of type Object). Let’s break down our composite adapter:

First function in our adapter is getItemViewType(). Here we can see how our model class, which was mentioned in Step 3, is used to determine which delegate adapter to use. Basically, it loops through all the delegate adapters and checks if there is any delegate adapter that can handle the type of model at given position. If there is one, then it returns a key from the sparse array mentioned above (as getItemViewType() has to return Int value). If it can’t find any delegate adapter, then exception is thrown saying it cannot get view type for a given position (meaning, there is no delegate adapter that can handle the type of model at a position).

Next, onCreateViewHolder() uses the key returned from getItemViewType() to find corresponding delegate adapter and call its createViewHolder() function. Function onBindViewHolder() acts the same way, it tries to find corresponding delegate adapter and throws exception if no adapter is found.

Three function at the end onViewRecycled(), onViewAttachedToWindow() and onViewDetachedFromWindow() are implemented optionally. They do not serve any purpose here, I’ve implemented them for display purposes only.

At the of the file we can see the Builder pattern which is used to create our composite adapter. Here we add delegate adapters to our composite adapter and then call build() function to create an instance of a composite adapter which is then passed to a RecyclerView. Variable count is used as a key and the delegate adapter itself is used as a value in the SparseArray which is then passed to a constructor.

Step 6 — Connecting RecyclerView

Our solution is now ready to be implemented into our application. What we need to do:

  1. Create model items.
  2. Create delegate adapters for each of the models.
  3. Create an instance of composite adapter and pass delegate adapters to it.
  4. Connect composite adapter to our RecyclerView.

And that’s basically it. Activity is written for display purposes only and I think it is better to create model items in your ViewModel and then pass it to the Activity or Fragment using LiveData<DelegateAdapterItem>, but this is for you to implement. :)

In case if you need to change the order of the ViewHolders, just change the order of the model items in the list. Whenever you need to add other types of layouts, just create one more model, create corresponding delegate adapter and add it to composite adapter using add() function. It is quite easy and we don’t have to touch any previously created adapters.

Nested RecyclerViews

The delegate adapters we have created here are powerful and generic enough to work with nested RecyclerViews without any changes in the adapter. Surprisingly, we can have a list of items inside our model class and create regular adapter inside our delegate adapter to handle this list. This way, we can have nested RecyclerViews inside ViewHolders.

Let’s consider example of such complex list with nested RecyclerViews. We will take Google Play Store as an example. It has many different view types and has nested RecyclerViews inside some of them. Firstly, we need to break down this screenshot to understand what it consists of. From the top to bottom:

  1. Tab bar — just a regular TabBarLayout.
  2. Banners— list of swipeable cards. Might be a ViewPager or RecyclerView.
  3. List of Car Games — swipeable list of car games with a title. TextView with an Arrow Button on top and RecyclerView at the bottom.
  4. Rating — rating view with an app name and a title.
  5. List of Non-stop action — same as item 3, but with different title and different list of games.

Now, let me try to show you how you could implement such screen.

We can clearly see, that we will have three view types and three adapters for each of them respectively:

  • BannerAdapterItem + BannerDelegateAdapter
  • GameListAdapterItem + GameListDelegateAdapter
  • RatingAdapterItem + RatingDelegateAdapter

Let’s have a look at the code, I will omit some details as this is just to show you how easy and intuitive it is to implement delegate adapters.

Code for Banner Item

data class BannerAdapterItem(
bannerList: List<Banner>
): DelegateAdapterItem {
//...
}
class BannerDelegateAdapter(
private val onBannerClicked: ((Banner) -> Unit)
): DelegateAdapter<BannerAdapterItem, ViewHolder>(BannerAdapterItem::class.java) {
//...
}

Code for Game List Item

data class GameListAdapterItem(
title: String,
games: List<Game>
): DelegateAdapterItem {
//...
}
class GameListDelegateAdapter(
private val onGameClicked: ((Game) -> Unit)
): DelegateAdapter<GameListAdapterItem, ViewHolder>(GameListAdapterItem::class.java) {
//...
}

Code for Rating Item

data class RatingAdapterItem(
game: Game
): DelegateAdapterItem {
//...
}
class RatingDelegateAdapter(
private val onRatingClicked: ((Int) -> Unit)
): DelegateAdapter<RatingAdapterItem, ViewHolder>(RatingAdapterItem::class.java) {
//...
}

Finally, code for your activity

class MainActivity: AppCompatActivity {    private val compositeAdapter by lazy {
CompositeAdapter.Builder()
.add(BannerAdapter(...))
.add(GameListDelegateAdapter(...))
.add(RatingDelegateAdapter(...))
.build()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val bannerItem = BannerAdapterItem(...)
val gameListItem = GameListAdapterItem(...)
val ratingItem = RatingAdapterItem(...)
val listOfItems = listOf<DelegateAdapterItem>(
bannerItem, gameListItem, ratingItem
)
compositeAdapter.submitList(listOfItems)
}
}

Summary

Let’s sum up what we have done here. We created several tiny adapters that will only handle one type of model, this corresponds to S.R.P principle of SOLID. We created composite adapter which stores these delegate adapters and in case if we need to add one more layout, we just add one more adapter. This conforms to Open-Closed principle of SOLID.

By separating the adapters based on view type, we achieved cleaner and more readable code, not to mention the ease of modification and expansion. I think that it’s a quite elegant solution which will help you decompose your code and write more complex RecyclerViews.

Check out my Github Repo which includes simple example of delegate adapter pattern implementation.

You can find me on Medium and LinkedIn.

If you enjoyed this story, please click the 👏 button and share to help others find it! Feel free to leave a comment below.

--

--