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.
- Chapter 1: RecyclerView with ItemClickListener in Kotlin
- Chapter 2: RecyclerView with Multiple View Types (this post)
- Chapter 3: RecyclerView with Endless Scrolling
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
whenexpressions exhaustive. If you add a new item type, the compiler will remind you to handle it ingetItemViewTypeandonBindViewHolder. - Use companion object constants for view types — never use raw integers like
0,1,2. Named constants likeVIEW_TYPE_HEADERmake 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 lists —
notifyDataSetChanged()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.
- 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