Recyclerview with Multiple View Types in Kotlin for Android

In many real-world Android apps, a single list needs to display different types of content — a header, regular items, ads, loading indicators, or featured cards. RecyclerView's multiple view types feature makes this possible by rendering different layouts for different data types within the same list.

In this guide — Chapter 2 of the RecyclerView series — you'll implement multiple view types using sealed classes, the ViewHolder pattern, and modern ViewBinding.

This is Chapter 2 of the RecyclerView series:
Android RecyclerView Multiple View Types Kotlin

Android RecyclerView with Multiple View Types in Kotlin

How Multiple View Types Work

RecyclerView supports multiple view types through two key methods in your adapter:

  • getItemViewType(position) — returns an integer identifying the view type at a given position
  • onCreateViewHolder(parent, viewType) — creates the correct ViewHolder based on the view type integer returned above

The cleanest way to model this in Kotlin is with a sealed class — it gives you a type-safe, exhaustive representation of all possible item types in your list.

Step 1: Define the Data Model with a Sealed Class

A sealed class ensures every item type is known at compile time. The when expression will give a compile error if you forget to handle a type — making your code safer:

// ListItem.kt
sealed class ListItem {
    data class HeaderItem(val title: String) : ListItem()
    data class TextItem(val text: String, val subtitle: String) : ListItem()
    data class ImageItem(val imageUrl: String, val caption: String) : ListItem()
    object LoadingItem : ListItem() // For pagination loading indicator
}

Step 2: Create Layout Files for Each View Type

item_header.xml — section header:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tvHeader"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp"
    android:textSize="18sp"
    android:textStyle="bold"
    android:background="#F5F5F5"/>

item_text.xml — standard text item:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

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

    <TextView
        android:id="@+id/tvSubtitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="13sp"
        android:layout_marginTop="4dp"/>

</LinearLayout>

item_image.xml — image item with caption:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/ivImage"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:scaleType="centerCrop"/>

    <TextView
        android:id="@+id/tvCaption"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:textSize="13sp"/>

</LinearLayout>

item_loading.xml — loading indicator for pagination:

<?xml version="1.0" encoding="utf-8"?>
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:padding="16dp"/>

Step 3: Add Glide Dependency

Add Glide to your build.gradle (app) for loading images:

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.16.0'
}

Step 4: Create ViewHolders with ViewBinding

Use ViewBinding in each ViewHolder instead of findViewById — it's null-safe, type-safe, and eliminates boilerplate. Enable ViewBinding in your build.gradle:

android {
    buildFeatures {
        viewBinding true
    }
}

Now create ViewHolders using the generated binding classes:

import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide

// Header ViewHolder
class HeaderViewHolder(
    private val binding: ItemHeaderBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: ListItem.HeaderItem) {
        binding.tvHeader.text = item.title
    }
}

// Text ViewHolder
class TextViewHolder(
    private val binding: ItemTextBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: ListItem.TextItem) {
        binding.tvText.text = item.text
        binding.tvSubtitle.text = item.subtitle
    }
}

// Image ViewHolder
class ImageViewHolder(
    private val binding: ItemImageBinding
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: ListItem.ImageItem) {
        Glide.with(binding.ivImage)
            .load(item.imageUrl)
            .placeholder(R.drawable.ic_placeholder)
            .into(binding.ivImage)
        binding.tvCaption.text = item.caption
    }
}

// Loading ViewHolder — no binding needed, no data to bind
class LoadingViewHolder(
    itemView: View
) : RecyclerView.ViewHolder(itemView)

Step 5: Create the Adapter

Use companion object constants instead of magic numbers for view types — makes code readable and prevents bugs:

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView

class MultiTypeAdapter(
    private val items: MutableList<ListItem> = mutableListOf()
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        const val VIEW_TYPE_HEADER = 0
        const val VIEW_TYPE_TEXT = 1
        const val VIEW_TYPE_IMAGE = 2
        const val VIEW_TYPE_LOADING = 3
    }

    // Returns the view type constant for each item position
    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ListItem.HeaderItem -> VIEW_TYPE_HEADER
            is ListItem.TextItem   -> VIEW_TYPE_TEXT
            is ListItem.ImageItem  -> VIEW_TYPE_IMAGE
            is ListItem.LoadingItem -> VIEW_TYPE_LOADING
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)
        return when (viewType) {
            VIEW_TYPE_HEADER  -> HeaderViewHolder(
                ItemHeaderBinding.inflate(inflater, parent, false)
            )
            VIEW_TYPE_TEXT    -> TextViewHolder(
                ItemTextBinding.inflate(inflater, parent, false)
            )
            VIEW_TYPE_IMAGE   -> ImageViewHolder(
                ItemImageBinding.inflate(inflater, parent, false)
            )
            VIEW_TYPE_LOADING -> LoadingViewHolder(
                inflater.inflate(R.layout.item_loading, parent, false)
            )
            else -> throw IllegalArgumentException("Unknown view type: $viewType")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (val item = items[position]) {
            is ListItem.HeaderItem  -> (holder as HeaderViewHolder).bind(item)
            is ListItem.TextItem    -> (holder as TextViewHolder).bind(item)
            is ListItem.ImageItem   -> (holder as ImageViewHolder).bind(item)
            is ListItem.LoadingItem -> { /* Nothing to bind for loading */ }
        }
    }

    override fun getItemCount() = items.size

    // Update list and refresh only changed items
    fun submitList(newItems: List<ListItem>) {
        items.clear()
        items.addAll(newItems)
        notifyDataSetChanged()
    }

    // Show/hide loading indicator at the bottom
    fun showLoading(show: Boolean) {
        if (show) {
            items.add(ListItem.LoadingItem)
            notifyItemInserted(items.size - 1)
        } else {
            val index = items.indexOfLast { it is ListItem.LoadingItem }
            if (index != -1) {
                items.removeAt(index)
                notifyItemRemoved(index)
            }
        }
    }
}

Step 6: Wire It Up in Activity

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: MultiTypeAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        adapter = MultiTypeAdapter()
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            this.adapter = this@MainActivity.adapter
        }

        loadData()
    }

    private fun loadData() {
        val items = listOf(
            ListItem.HeaderItem("Featured Articles"),
            ListItem.ImageItem("https://example.com/img1.jpg", "Android 15 Overview"),
            ListItem.ImageItem("https://example.com/img2.jpg", "Kotlin 2.0 Features"),
            ListItem.HeaderItem("Latest Posts"),
            ListItem.TextItem("ViewModel State Management", "Learn LiveData and StateFlow"),
            ListItem.TextItem("RecyclerView Endless Scrolling", "Paginated list loading"),
            ListItem.TextItem("Firebase Remote Config", "Dynamic app configuration"),
        )
        adapter.submitList(items)
    }
}

Best Practices

  • Use sealed classes for item types — they make when expressions exhaustive. If you add a new item type, the compiler will remind you to handle it in getItemViewType and onBindViewHolder.
  • Use companion object constants for view types — never use raw integers like 0, 1, 2. Named constants like VIEW_TYPE_HEADER make the code self-documenting and prevent bugs.
  • Use ViewBinding over findViewById — ViewBinding is null-safe, generates no overhead, and eliminates the classic NPE from mismatched IDs.
  • Consider DiffUtil for large listsnotifyDataSetChanged() redraws the entire list. Use DiffUtil or ListAdapter to only redraw changed items for better performance.
  • Keep ViewHolder logic minimal — ViewHolder should only bind data to views. Move business logic to the ViewModel or a click listener passed into the adapter.

Frequently Asked Questions

Why use sealed classes for RecyclerView multiple view types?
Sealed classes make when expressions exhaustive — the compiler warns you if you forget to handle a type. This prevents runtime crashes from unhandled view types. They also serve as a clean, type-safe data model for your list items.

How many view types can a RecyclerView have?
Technically unlimited, but RecyclerView maintains a separate pool of recycled views for each view type. Too many view types reduces recycling efficiency. In practice, keep it to 3–5 view types for optimal performance.

What is the difference between RecyclerView and ListView for multiple view types?
Both support multiple view types via getItemViewType(). RecyclerView is the modern recommended approach — it supports DiffUtil for efficient updates, LayoutManagers for flexible layouts, and ItemAnimators for smooth animations. ListView is legacy and should not be used in new projects.

How do I handle click listeners with multiple view types?
Pass a lambda or interface into the adapter constructor for each clickable item type. In onBindViewHolder, set the click listener on the ViewHolder's itemView or specific views. Avoid setting click listeners in onCreateViewHolder as it captures a stale position.

📚 Continue Learning
📝 Summary
  • Use a sealed class to model all item types — compile-time safety
  • Override getItemViewType() to return the correct type per position
  • Use companion object constants instead of magic numbers for view types
  • Use ViewBinding in ViewHolders instead of findViewById
  • Add a LoadingItem type for pagination loading indicators
  • Use DiffUtil or ListAdapter for efficient list updates
  • Keep it to 3–5 view types max for optimal RecyclerView recycling performance

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

Post a Comment

Please let us know about any concerns or query.

Previous Post Next Post

Contact Form