In an Android feed that mixes titles, cards, carousels, and CTAs, a single-layout RecyclerView is not enough. The practical problem is: how do you support many cell types without a giant adapter full of when (viewType), and without scattering ViewHolders far from the models they bind?
That is what a multi-type (delegate) adapter is for. Keep the adapter thin; make each row type own its layout, ViewHolder, and binding. This post is about that idea. That problem is what led me to open-source MultiTypeAdapter in 2020 β later evolved as DelegateAdapter β while building modular Android apps with changing home feeds.
Why this problem shows up
The usual RecyclerView setup works well for one layout. As soon as the list becomes heterogeneous, things get messy:
- One adapter class with a long
when (viewType)chain - ViewHolder classes scattered across packages, far from the model they bind
- Click handling duplicated or passed through awkward callbacks
When a feed changes often, you want to add a new row type by adding a new model β not by editing a central adapter every time.
The core idea
Each list item is a model that implements a binding contract (in the example library, ViewHolderBinding). The model declares three things in one place:
- Layout β which XML resource to inflate
- ViewHolder β how to bind data to views
- Factory β how to create that ViewHolder when the adapter needs it
The adapter stays generic. It inflates the right layout, creates the right ViewHolder, and binds the model. No central switch statement growing with every new section type.
βββββββββββββββββββββββββββββββββββββββββββ β Activity / Fragment β β MultiTypeAdapter + RecyclerView β βββββββββββββββββββββββββββββββββββββββββββ€ β List of models (heterogeneous) β β TitleModel Β· MainCardModel Β· β¦ β βββββββββββββββββββββββββββββββββββββββββββ€ β Each model implements the binding API β β layout + ViewHolder + onCreateβ¦ β βββββββββββββββββββββββββββββββββββββββββββ
Setup in an Activity
Initialize the adapter, attach it to a RecyclerView, then push models when your data is ready:
multiTypeAdapter = MultiTypeAdapter()
recyclerView.adapter = multiTypeAdapter
recyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
multiTypeAdapter.setModels(
listOf(TitleModel(title = "Section #1"))
)
The constructor also accepts an optional listener (for clicks) and an initial list. Most of the time the defaults are enough until you need interaction.
A simple row type
A section title is a good minimal example. The model carries the data and owns its ViewHolder:
data class TitleModel(
val title: String
) : ViewHolderBinding {
override val layout: Int
get() = R.layout.item_title
override fun onCreateViewHolder(
itemView: View,
listener: ViewHolderBinding.Listener
) = TitleViewHolder(itemView)
class TitleViewHolder(itemView: View)
: BaseViewHolder<TitleModel>(itemView) {
override fun bind(model: TitleModel) {
itemView.title.text = model.title
}
}
}
Adding a new row type means adding a new class like this β not opening the adapter and adding another branch.
Clicks without spaghetti callbacks
Interactive rows need to talk back to the screen. One clean pattern: define a listener interface that extends a shared listener base, pass it into the adapter constructor, and cast it inside onCreateViewHolder when building the ViewHolder.
class MainCardModel(
val title: String,
@DrawableRes val image: Int
) : ViewHolderBinding {
interface CustomListener : ViewHolderBinding.Listener {
fun onClick(model: MainCardModel)
}
override fun onCreateViewHolder(
itemView: View,
listener: ViewHolderBinding.Listener
) = MainViewHolder(itemView, listener as CustomListener)
// β¦ layout, ViewHolder with itemView.setOnClickListener { β¦ }
}
multiTypeAdapter = MultiTypeAdapter(
listener = object : MainCardModel.CustomListener {
override fun onClick(model: MainCardModel) {
// navigate, open detail, etc.
}
}
)
One listener object at the adapter level, typed per row where it matters. The Activity stays the place that decides what a click does; the ViewHolder only reports that it happened.
One list, mixed types
Once each model implements the contract, the feed is just a list:
multiTypeAdapter.setModels(
listOf(
TitleModel(title = "Section #1"),
MainCardModel(
title = "Main Landscape",
image = R.drawable.image
)
)
)
That matches how product thinks about the screen β an ordered sequence of blocks β not as βview type 3 after view type 1.β
What to take away
- Co-locate what changes together. Layout, binding, and ViewHolder for a row belong in one unit.
- Keep the adapter agnostic. It should not know about app-specific sections β only about the binding contract.
- Composition beats inheritance. New row types are new models, not subclasses of a mega-adapter.
- This pattern has many names. Adapter delegates, multi-type lists, Epoxy β the goal is the same: scalable heterogeneous lists.
Today I might reach for View Binding, Compose lazy lists, or a mature delegate library depending on the project. The lesson still applies: make each row type self-contained, and keep the adapter thin.
Try the demo
An open-source implementation and Android demo:
- DelegateAdapter β evolved from the original MultiType adapter
I first wrote about this on Medium in 2020; this version is a shorter, architecture-focused update.
I build native iOS and Android apps and share notes like this as I ship. If mobile architecture is your thing too, let's connect on LinkedIn.