All the apps nowadays include some sort of list one way or another. Whether the main view of your app is a list or not, one common use case is to have a list displaying multiple types of items. Then you look at the examples provided by Google and they just show some simple case where you have a plain list with no variation between its elements, which is a valid case but not so common as apps get more and more complex.

Displaying multiple items in the same RecyclerView is easier than you think, let's get started.

Getting Ready

To use a RecyclerView in your project you need to import the following androidx dependency on the build.gradle file for your app module:

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
}

💎 Note that we are not using the old support library com.android.support:recyclerview-v7 as it is deprecated in favor of androidx.

Then you can add a RecyclerView on your layout, for instance:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
										   xmlns:app="http://schemas.android.com/apk/res-auto"
										   android:id="@+id/recyclerview"
										   android:scrollbars="vertical"
										   android:layout_width="match_parent"
										   android:layout_height="match_parent"
										   app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

💎 Tip: You can add the type of LayoutManager directly on the xml. This way you skip a line of code or two in your class 😉.

So once that's in place, we need to setup the adapter and our first ViewHolder:

class ItemsAdapter(private val descriptionList: List<String>) : RecyclerView.Adapter<ItemViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
        val cardView = LayoutInflater.from(parent.context).inflate(R.layout.viewholder_item, parent, false) as CardView
        return ItemViewHolder(cardView)
    }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) = with(holder.card) {
        title.text = "Title $position"
        description.text = descriptionList[position]
    }

    override fun getItemCount() = descriptionList.size
}
class ItemViewHolder(val card: View) : RecyclerView.ViewHolder(card)
<?xml version="1.0" encoding="utf-8"?>
<app:androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
									   xmlns:app="http://schemas.android.com/apk/res-auto"
									   android:layout_width="match_parent"
									   android:layout_height="200dp"
									   app:cardCornerRadius="4dp"
									   android:layout_margin="16dp">

	<LinearLayout android:orientation="vertical"
				  android:padding="16dp"
				  android:layout_width="match_parent"
				  android:layout_height="wrap_content">

		<TextView
				android:layout_width="match_parent"
				android:layout_height="wrap_content"
				android:textSize="16sp"
				android:textStyle="bold"
				android:id="@+id/title"/>

		<TextView
				android:layout_width="match_parent"
				android:layout_height="wrap_content"
				android:id="@+id/description"/>
	</LinearLayout>


</app:androidx.cardview.widget.CardView>

Great, now let's see how this will look like all together in our Activity:

class MainActivity : AppCompatActivity() {

    private lateinit var recyclerView: RecyclerView
    private val itemsAdapter = ItemsAdapter(descriptions)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        recyclerView = findViewById<RecyclerView>(R.id.recyclerview).apply {
            setHasFixedSize(true)
            this.adapter = itemsAdapter
        }
    }

    companion object {
        private val descriptions = listOf(
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
            "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
            "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat",
            "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur"
        )
    }
}

So as you can see, we have a MainActivity which has a RecyclerView with a list of cards represented by the ItemViewHolder. Let's see how the result looks like:

RecyclerView with a single ViewHolder 

Great! That's quite simple. But what would happen if we wanted to have some other item instead, together with the existing ViewHolder?

Displaying two ViewHolders with the same Adapter

Now let's define another ViewHolder which will act as a header for our list:

class HeaderViewHolder(val header: TextView) : RecyclerView.ViewHolder(header)

It will be a simple TextView, so nothing fancy to see here 😊.

But how do we make the adapter to be able to display both HeaderViewHolder and ItemViewHolder?

So let's make the adapter a bit more generic, so instead of taking a single type of ViewHolder, it can take something more broad. In this case, we will make it take any RecyclerView.ViewHolder, because that what both of our ViewHolders extend.

💎 Tip: It could be whatever, not just a RecyclerView.ViewHolder. You could create an empty interface which those items would extend and you could bulk them into the same adapter... do you see where we are going?

To begin with, I renamed ItemViewHolder to CardViewHolder to make it a bit more clear. So we have:

class CardViewHolder(val card: CardView) : RecyclerView.ViewHolder(card)

Then in the adapter we need to override getItemViewType and adjust the onCreateViewHolder and onBindViewHolder functions in order to handle both possible viewholders:

class ItemsAdapter(private val descriptionList: List<ListItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    override fun getItemViewType(position: Int): Int {
        return when (descriptionList[position]) {
            is HeaderItem -> HEADER_TYPE
            is CardItem -> CARD_TYPE
            else -> super.getItemViewType(position)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            HEADER_TYPE -> {
                val header = LayoutInflater.from(parent.context).inflate(R.layout.viewholder_header, parent, false) as TextView
                HeaderViewHolder(header)
            }
            CARD_TYPE -> {
                val cardView =
                    LayoutInflater.from(parent.context).inflate(R.layout.viewholder_card, parent, false) as CardView
                CardViewHolder(cardView)
            }
            else -> TODO("Type $viewType not implemented")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = with(holder) {
        when (this) {
            is HeaderViewHolder -> header.text = (descriptionList[position] as HeaderItem).header
            is CardViewHolder -> {
                card.title.text = (descriptionList[position] as CardItem).title
                card.description.text = (descriptionList[position] as CardItem).description
            }
        }
    }

    override fun getItemCount() = descriptionList.size

    companion object {
        const val HEADER_TYPE = 0
        const val CARD_TYPE = 1
    }
}

Note that I getItemViewType will be invoked before onCreateViewHolder, so the return type of the former is exactly the one that goes into the latter. We check which viewType we got and create viewholders accordingly.

Now the adapter now takes a list of ListItem. This is just an empty interface defined to have a common type that can be handled by the adapter. Very useful. You could pretty much provide any object extending ListItem and then add the corresponding logic to handle it.

Ultimately, we need to adjust the descriptions list in our MainActivity. Before it was a list of strings, we need to change this to a list of ListItem:

private val descriptions = listOf(
            HeaderItem("First Category"),
            CardItem("Title 1", "Description 1"),
            CardItem("Title 2", "Description 2"),
            CardItem("Title 3", "Description 3"),
            HeaderItem("Second Category"),
            CardItem("Title 4", "Description 4"),
            CardItem("Title 5", "Description 5")
        )

I also did some minor layout adjustments, so at the end it looks like this:

Final list with two types of ViewHolders

There is of course room for improvement but this is just a simple example to show you the possibilities out there.

Do you want to have multiple, and I mean 3 or more, viewholders? No problem, but I don't advice you to do it this way then, otherwise it could be extremely messy, hard to test and maintain.

In such case, I recommend you to look at the delegate pattern and the adapter-delegates library Hannes Dorfmann created. It allows you to gracefully handle such case, which I am going to cover in another post together with meta-programming, so stay tuned 😉.

You can find the source code of this tutorial on my GitHub.