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.
- Chapter 1: RecyclerView with ItemClickListener (this post)
- Chapter 2: RecyclerView with Multiple View Types in Kotlin
- Chapter 3: RecyclerView with Endless Scrolling in Kotlin
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>
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>
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.Adapter —
ListAdapterhas built-inDiffUtilsupport. It automatically calculates the difference between old and new lists and only animates changed items. This is dramatically more efficient thannotifyDataSetChanged(). - Use ViewBinding over findViewById — null-safe, type-safe, and no runtime overhead. Enable it in your
build.gradlewith one line. - Pass click listeners via constructor — don't set click listeners in
onCreateViewHolder(captures stale position). Set them inonBindViewHoldervia thebind()method so they always reference the current item. - Use DiffUtil.ItemCallback correctly —
areItemsTheSameshould compare unique IDs, not content.areContentsTheSameshould compare all fields. For data classes,==inareContentsTheSameworks perfectly. - Add item spacing with ItemDecoration — use
DividerItemDecorationfor dividers or create a customSpaceItemDecorationfor 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.
- 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 inareContentsTheSame
nicely explained. Thank you for sharing.
ReplyDelete