Step 6: Mastering RecyclerView in Android - Efficient List Rendering with Kotlin

📚 Learn Android with Kotlin — Series
1. Kotlin 2. Android Studio 3. Activities 4. Intents 5. Fragments 6. RecyclerView 7. LiveData 8. Room 9. Retrofit 🔒 10. Material 🔒 11. Firebase 🔒 12. Testing 🔒 13. Publish 🔒

In Step 5 you learned how Fragments let you build modular UIs. But as soon as your app needs to display a list of items — a feed, a contact list, a product catalogue — you need something purpose-built for that. Enter RecyclerView.

RecyclerView is the modern replacement for the deprecated ListView. It's faster, more flexible, and handles large datasets efficiently by reusing (recycling) item views as they scroll off screen. This step covers everything from the basics to a production-ready implementation with click listeners, DiffUtil, and multiple layout managers.

Android RecyclerView with Kotlin — efficient list rendering

RecyclerView — reuses views as they scroll off screen for maximum performance

Prerequisites: You should be comfortable with Activities and Layouts (Step 3) and Fragments (Step 5) before starting this guide.

What Is RecyclerView and Why Use It?

The name tells the story — RecyclerView recycles item views. When an item scrolls off screen, its View isn't discarded. It gets handed to the next incoming item and rebound with new data. This keeps memory flat regardless of how many items are in your list.

RecyclerView ListView
ViewHolder pattern ✅ Required — enforced by API ⚠️ Optional — often skipped, causes jank
Efficient updates ✅ DiffUtil — only redraws changed items ❌ Full redraw on every data change
Layout flexibility ✅ Linear, Grid, Staggered Grid ❌ Vertical linear only
Animations ✅ Built-in item add/remove animations ❌ Manual implementation
Status ✅ Actively maintained Jetpack library ❌ Deprecated — do not use in new code

1. The Three Components You Must Know

RecyclerView is built on three collaborating pieces. Understand their roles before writing any code:

  • RecyclerView — the scrollable container you place in your layout XML. It manages scrolling and requests views from the Adapter when needed.
  • RecyclerView.Adapter — the bridge between your data and the UI. It creates ViewHolders and binds data to them at the right moment.
  • RecyclerView.ViewHolder — caches references to views for a single list item. Without it, Android calls findViewById() on every scroll event — which is slow.

2. Add Dependencies and Enable ViewBinding

Add RecyclerView to build.gradle (app) and enable ViewBinding — it replaces findViewById with null-safe, type-safe generated code:

android {
    buildFeatures {
        viewBinding true
    }
}

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

3. Create the Layouts

Add RecyclerView to activity_main.xml:

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

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="8dp"
        android:clipToPadding="false"/>

</FrameLayout>

Create item_user.xml — the layout for each row in the list:

<?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="horizontal"
    android:padding="16dp"
    android:gravity="center_vertical">

    <ImageView
        android:id="@+id/ivAvatar"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@drawable/ic_person"
        android:scaleType="centerCrop"/>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical"
        android:layout_marginStart="16dp">

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

        <TextView
            android:id="@+id/tvEmail"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="13sp"
            android:layout_marginTop="2dp"/>

    </LinearLayout>
</LinearLayout>

4. Create the Data Model

// User.kt
data class User(
    val id: Int,       // Unique identifier — used by DiffUtil
    val name: String,
    val email: String
)

5. Build the Adapter with ListAdapter and DiffUtil

Use ListAdapter instead of the base RecyclerView.Adapter. It has DiffUtil built in — it calculates the difference between old and new lists on a background thread and only redraws items that actually changed. The result is smooth, flicker-free updates even with large lists.

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

class UserAdapter(
    private val onItemClick: (User) -> Unit
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {

    inner class ViewHolder(
        private val binding: ItemUserBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(user: User) {
            binding.tvName.text = user.name
            binding.tvEmail.text = user.email

            // Set click listener here in bind() — not in onCreateViewHolder
            // onCreateViewHolder captures a stale position
            binding.root.setOnClickListener { onItemClick(user) }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemUserBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

// DiffUtil — tells RecyclerView exactly which items changed
class UserDiffCallback : DiffUtil.ItemCallback<User>() {

    // Same item = same unique ID (not the same content)
    override fun areItemsTheSame(oldItem: User, newItem: User) =
        oldItem.id == newItem.id

    // Same content = no visual update needed
    override fun areContentsTheSame(oldItem: User, newItem: User) =
        oldItem == newItem  // data class == compares all fields
}

6. Set Up RecyclerView in Activity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: UserAdapter

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

        setupRecyclerView()
        loadData()
    }

    private fun setupRecyclerView() {
        adapter = UserAdapter { user ->
            Toast.makeText(this, "Clicked: ${user.name}", Toast.LENGTH_SHORT).show()
        }

        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(this@MainActivity)
            this.adapter = this@MainActivity.adapter
            setHasFixedSize(true) // Free performance win — size doesn't change with content
        }
    }

    private fun loadData() {
        val users = listOf(
            User(1, "Alice Johnson", "alice@example.com"),
            User(2, "Bob Smith", "bob@example.com"),
            User(3, "Carol White", "carol@example.com"),
            User(4, "David Brown", "david@example.com"),
            User(5, "Eve Davis", "eve@example.com"),
        )
        adapter.submitList(users) // DiffUtil handles the diff automatically
    }
}

7. Switching Layouts — Linear, Grid, and Staggered Grid

Swap the LayoutManager to change how your list renders — the Adapter stays exactly the same:

// Vertical list (default)
binding.recyclerView.layoutManager = LinearLayoutManager(this)

// Horizontal scrolling list (like a carousel)
binding.recyclerView.layoutManager =
    LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)

// 2-column grid
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)

// Staggered grid — items can be different heights (Pinterest-style)
binding.recyclerView.layoutManager =
    StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)

8. Item Spacing with ItemDecoration

Never add margin to item layouts for spacing — it applies inconsistently to first/last items. Use ItemDecoration instead:

// Built-in divider line between items
binding.recyclerView.addItemDecoration(
    DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)

// Custom equal spacing between all items
class SpaceItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect, view: View,
        parent: RecyclerView, state: RecyclerView.State
    ) {
        outRect.bottom = space
        // Add top spacing only for the first item if needed
        if (parent.getChildAdapterPosition(view) == 0) {
            outRect.top = space
        }
    }
}

// Usage
binding.recyclerView.addItemDecoration(
    SpaceItemDecoration(resources.getDimensionPixelSize(R.dimen.item_space))
)

Best Practices

  • Use ListAdapter + DiffUtil — not notifyDataSetChanged()notifyDataSetChanged() redraws the entire list every time, loses scroll position, and kills animations. ListAdapter + DiffUtil only touches what changed. The visual difference is immediately obvious.
  • Use ViewBinding over findViewById — null-safe, type-safe, zero runtime overhead. Enable it once in Gradle and it generates binding classes for every layout automatically.
  • Pass click listeners as lambdas in the constructor — never set click listeners in onCreateViewHolder. It captures a stale adapter position. Always set them inside bind().
  • Keep ViewHolder logic minimal — the ViewHolder should only set text and images. Business logic, data fetching, and state management belong in a ViewModel.
  • Use setHasFixedSize(true) when possible — if adding or removing items doesn't change the RecyclerView's own size in the layout, this is a free performance improvement.

Try It Yourself

  1. Add a delete action — add a delete button to each item view. When clicked, remove that user from the list and call adapter.submitList() with the updated list. Observe how DiffUtil animates only the removed item.
  2. Switch to a grid — change the LayoutManager to GridLayoutManager(this, 2) and update your item layout to look good in a 2-column grid. Run the app — the Adapter needs zero changes.
  3. Add a search filter — add an EditText above the RecyclerView. As the user types, filter the list by name and call adapter.submitList(filteredList). Watch DiffUtil animate the items in and out.

Frequently Asked Questions

What is the difference between RecyclerView.Adapter and ListAdapter?
RecyclerView.Adapter is the base class — you manage the list manually and trigger a full redraw with notifyDataSetChanged(). ListAdapter has DiffUtil built in, runs the diff calculation on a background thread, and only redraws items that actually changed. Always use ListAdapter.

Why does my RecyclerView flicker when I update data?
You're calling notifyDataSetChanged(), which redraws the entire list and loses scroll position. Switch to ListAdapter + DiffUtil and call adapter.submitList(newList) — only changed items are redrawn, with smooth animations.

How do I make a RecyclerView scroll horizontally?
Set LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) as the LayoutManager. Your Adapter doesn't need any changes — LayoutManager and Adapter are completely independent.

Can I have multiple view types in one RecyclerView?
Yes — override getItemViewType(), inflate the correct layout in onCreateViewHolder based on the type integer, and bind correctly in onBindViewHolder. See our RecyclerView Multiple View Types guide for the full implementation with sealed classes.

📚 Continue the Series
📝 Step 6 Summary
  • RecyclerView recycles views — memory stays flat regardless of list size
  • Three parts: RecyclerView (container) + Adapter (data bridge) + ViewHolder (view cache)
  • Use ListAdapter + DiffUtil — only redraws changed items, smooth animations, no flicker
  • Use ViewBinding in ViewHolder — null-safe, no findViewById()
  • Set click listeners inside bind() — never in onCreateViewHolder()
  • Swap LayoutManager to switch between linear, grid, and staggered layouts
  • Use ItemDecoration for spacing — never add margins to item layouts
  • setHasFixedSize(true) — free performance improvement when list size is stable

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