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:

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:

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

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:

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.