Android RecyclerView with ItemClickListener in Kotlin Example

RecyclerView is the backbone of almost every Android app that displays lists. Knowing how to set it up efficiently — with a clean click listener, ViewBinding, and DiffUtil — is one of the most fundamental skills in Android development.

In this guide — Chapter 1 of the RecyclerView series — you'll build a fully working RecyclerView with item click listener in Kotlin, using modern best practices including ViewBinding and ListAdapter with DiffUtil.

This is Chapter 1 of the RecyclerView series:
Android RecyclerView with ItemClickListener in Kotlin demonstrating item click handling in a list

RecyclerView item click handling in Kotlin using an ItemClickListener for list interactions

What is RecyclerView?

RecyclerView is the modern, recommended way to display large lists of data in Android. It's called "Recycler" because it reuses (recycles) item views that scroll off screen instead of creating new ones — making it memory-efficient even for thousands of items.

The three key components you'll work with:

  • RecyclerView — the container widget placed in your layout
  • RecyclerView.Adapter — provides the data and creates ViewHolders
  • RecyclerView.ViewHolder — holds references to views for a single list item

Step 1: Add Dependency and Enable ViewBinding

Add RecyclerView to your build.gradle (app) and enable ViewBinding:

android {
    buildFeatures {
        viewBinding true // Enables ViewBinding for all layouts
    }
}

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

Step 2: Create the Layouts

Add RecyclerView to activity_main.xml. Use the tools: namespace to preview how your list will look in Android Studio without running the app:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    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"
        tools:listitem="@layout/item_layout"
        tools:itemCount="6"/>

</androidx.constraintlayout.widget.ConstraintLayout>
RecyclerView layout in Android Studio

RecyclerView layout in Android Studio — Credit: Android Academics

Create item_layout.xml for each list item:

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

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/imageView"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@tools:sample/avatars"/>

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:textColor="@color/black"
        android:textSize="16sp"
        tools:text="@tools:sample/first_names"/>

</LinearLayout>
RecyclerView item layout file

RecyclerView item layout file — Credit: Android Academics

RecyclerView tools preview in Android Studio

RecyclerView preview using tools namespace — Credit: Android Academics

Step 3: Create the Data Model

// User.kt
data class User(
    val id: Int,
    val name: String,
    val avatarRes: Int // drawable resource ID
)

Step 4: Create the Adapter with ViewBinding

Use ViewBinding instead of findViewById — it's null-safe, type-safe, and eliminates boilerplate. Pass click listeners as lambdas into the adapter constructor:

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,
    private val onItemLongClick: (User) -> Boolean = { false }
) : ListAdapter<User, UserAdapter.ViewHolder>(UserDiffCallback()) {

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

        fun bind(user: User) {
            binding.tvName.text = user.name
            binding.imageView.setImageResource(user.avatarRes)

            // Single click
            binding.root.setOnClickListener {
                onItemClick(user)
            }

            // Long press
            binding.root.setOnLongClickListener {
                onItemLongClick(user)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemLayoutBinding.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
// Only redraws changed items instead of the entire list
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User) =
        oldItem.id == newItem.id  // Same item = same ID

    override fun areContentsTheSame(oldItem: User, newItem: User) =
        oldItem == newItem  // Same content = identical data class
}

Step 5: Set Up RecyclerView in Activity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration
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(
            onItemClick = { user ->
                // Handle single click
                Toast.makeText(this, "Clicked: ${user.name}", Toast.LENGTH_SHORT).show()
            },
            onItemLongClick = { user ->
                // Handle long press
                Toast.makeText(this, "Long pressed: ${user.name}", Toast.LENGTH_SHORT).show()
                true // Return true to consume the event
            }
        )

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

            // Add divider between items
            addItemDecoration(
                DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
            )
        }
    }

    private fun loadData() {
        val users = listOf(
            User(1, "Alice Johnson", R.drawable.avatar1),
            User(2, "Bob Smith", R.drawable.avatar2),
            User(3, "Carol White", R.drawable.avatar3),
            User(4, "David Brown", R.drawable.avatar4),
            User(5, "Eve Davis", R.drawable.avatar5),
        )
        // submitList triggers DiffUtil automatically
        adapter.submitList(users)
    }
}

Step 6: Multiple Click Targets on a Single Item

Often you need different actions for different parts of the same item — e.g. clicking the avatar opens the profile, clicking the name opens a detail screen, clicking a button performs an action. Pass separate lambdas for each:

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

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

        fun bind(user: User) {
            binding.tvName.text = user.name
            binding.imageView.setImageResource(user.avatarRes)

            // Each view has its own click action
            binding.tvName.setOnClickListener { onNameClick(user) }
            binding.imageView.setOnClickListener { onAvatarClick(user) }
            binding.btnFollow.setOnClickListener { onFollowClick(user) }
        }
    }

    // ... rest of adapter
}

// In Activity
val adapter = UserAdapter(
    onNameClick = { user -> openDetailScreen(user) },
    onAvatarClick = { user -> openProfilePhoto(user) },
    onFollowClick = { user -> viewModel.followUser(user) }
)

Best Practices

  • Use ListAdapter over RecyclerView.AdapterListAdapter has built-in DiffUtil support. It automatically calculates the difference between old and new lists and only animates changed items. This is dramatically more efficient than notifyDataSetChanged().
  • Use ViewBinding over findViewById — null-safe, type-safe, and no runtime overhead. Enable it in your build.gradle with one line.
  • Pass click listeners via constructor — don't set click listeners in onCreateViewHolder (captures stale position). Set them in onBindViewHolder via the bind() method so they always reference the current item.
  • Use DiffUtil.ItemCallback correctlyareItemsTheSame should compare unique IDs, not content. areContentsTheSame should compare all fields. For data classes, == in areContentsTheSame works perfectly.
  • Add item spacing with ItemDecoration — use DividerItemDecoration for dividers or create a custom SpaceItemDecoration for padding between items. Never add margins to item layouts for spacing — it breaks the last item.

Frequently Asked Questions

Why use ListAdapter instead of RecyclerView.Adapter?
ListAdapter has built-in DiffUtil support which calculates differences between old and new lists on a background thread and only animates changed items. RecyclerView.Adapter with notifyDataSetChanged() redraws the entire list every time, which is inefficient and loses item animations.

How do I pass click events from RecyclerView adapter to Activity?
Pass a lambda function into the adapter's constructor. When an item is clicked, invoke the lambda with the clicked item as a parameter. In the Activity, provide the implementation of the lambda to handle the click event — as shown in Step 5 above.

What is DiffUtil in RecyclerView?
DiffUtil calculates the difference between two lists and outputs the minimal set of update operations needed. It runs on a background thread and animates only changed items — giving a smooth update experience instead of a jarring full-list redraw.

How do I add space between RecyclerView items?
Use ItemDecoration — either the built-in DividerItemDecoration for dividers, or a custom ItemDecoration subclass that overrides getItemOffsets() to add padding. Avoid using layout margins on item views for spacing as it doesn't work correctly for all items.

📚 Continue Learning
📝 Summary
  • Use ListAdapter with DiffUtil — only redraws changed items, enables animations
  • Use ViewBinding in ViewHolder — null-safe, no findViewById
  • Pass click listeners as lambdas in the adapter constructor
  • Set click listeners in onBindViewHolder — never in onCreateViewHolder
  • Use DividerItemDecoration or custom ItemDecoration for item spacing
  • In DiffUtil — compare IDs in areItemsTheSame, full equality in areContentsTheSame

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

1 Comments

Please let us know about any concerns or query.

  1. nicely explained. Thank you for sharing.

    ReplyDelete
Previous Post Next Post

Contact Form