RecyclerView Item onClickListener - Android

RecyclerView doesn't give you a built-in setOnItemClickListener() like ListView did. That was a deliberate design decision — RecyclerView gives you full control over how clicks are handled, which means you can do things ListView never could: click different parts of an item differently, handle long press, handle double tap, or pass typed data instead of a raw View to your callback.

The trade-off is that there's no single "correct" way to do it — there are a few patterns, each suited to different situations. This guide covers all of them in Kotlin with ViewBinding, from the simplest lambda to the cleanest interface callback pattern.

RecyclerView item click listener Android

RecyclerView item click — multiple approaches, each suited to different use cases

Approach 1 — Lambda in Adapter Constructor (Simplest)

Pass the click handler as a lambda when creating the adapter. Clean, simple, no interface needed. Best when you have one click action per item:

class UserAdapter(
    private val users: List<User>,
    private val onItemClick: (User) -> Unit  // lambda callback
) : RecyclerView.Adapter<UserAdapter.ViewHolder>() {

    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 on the whole item
            binding.root.setOnClickListener {
                onItemClick(user)  // pass the item, not the View
            }
        }
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) =
        holder.bind(users[position])

    override fun getItemCount() = users.size
}
// In your Activity or Fragment — pass the lambda when creating adapter
val adapter = UserAdapter(users) { user ->
    // Handle click — user is fully typed, no casting
    Toast.makeText(this, "Clicked: ${user.name}", Toast.LENGTH_SHORT).show()
    navigateToDetail(user.id)
}
binding.recyclerView.adapter = adapter
💡 Pass the item, not the View. The old Java pattern passed a View to the callback and called getTag() to retrieve the item. In Kotlin, pass the typed item directly — no tags, no casting, no null checks.

Approach 2 — Interface Callback (Most Flexible)

Define an interface with one or more callback methods. This is the cleanest pattern when you need multiple click actions per item — for example, clicking the item vs clicking a delete button within it:

// Define the interface
interface OnUserClickListener {
    fun onUserClick(user: User)
    fun onUserLongClick(user: User): Boolean
    fun onDeleteClick(user: User)
}
class UserAdapter(
    private val users: MutableList<User>,
    private val listener: OnUserClickListener
) : RecyclerView.Adapter<UserAdapter.ViewHolder>() {

    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

            // Item click
            binding.root.setOnClickListener {
                listener.onUserClick(user)
            }

            // Long press on item
            binding.root.setOnLongClickListener {
                listener.onUserLongClick(user)
            }

            // Click on the delete icon within the item
            binding.btnDelete.setOnClickListener {
                listener.onDeleteClick(user)
            }
        }
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) =
        holder.bind(users[position])

    override fun getItemCount() = users.size
}
// Implement the interface in your Fragment
class UsersFragment : Fragment(), OnUserClickListener {

    private lateinit var adapter: UserAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        adapter = UserAdapter(userList, this)
        binding.recyclerView.adapter = adapter
    }

    override fun onUserClick(user: User) {
        findNavController().navigate(
            UsersFragmentDirections.actionToDetail(user.id)
        )
    }

    override fun onUserLongClick(user: User): Boolean {
        showContextMenu(user)
        return true  // consume the event
    }

    override fun onDeleteClick(user: User) {
        viewModel.deleteUser(user)
    }
}

Approach 3 — Multiple Click Zones on One Item

This is where RecyclerView really shines over ListView. You can set independent click listeners on different views within the same item — the image does one thing, the title does another, a button does something else entirely:

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

    fun bind(
        article: Article,
        onArticleClick: (Article) -> Unit,
        onBookmarkClick: (Article) -> Unit,
        onShareClick: (Article) -> Unit
    ) {
        binding.tvTitle.text = article.title
        binding.tvAuthor.text = article.author

        Glide.with(binding.root)
            .load(article.imageUrl)
            .into(binding.imgThumbnail)

        // Clicking the thumbnail opens the article
        binding.imgThumbnail.setOnClickListener {
            onArticleClick(article)
        }

        // Clicking the title also opens the article
        binding.tvTitle.setOnClickListener {
            onArticleClick(article)
        }

        // Clicking bookmark icon saves the article
        binding.btnBookmark.setOnClickListener {
            onBookmarkClick(article)
        }

        // Clicking share icon opens share sheet
        binding.btnShare.setOnClickListener {
            onShareClick(article)
        }
    }
}

Approach 4 — ListAdapter with DiffUtil (Modern Best Practice)

ListAdapter is the modern replacement for RecyclerView.Adapter. It handles list diffing automatically — only updating rows that actually changed instead of calling notifyDataSetChanged() on everything. Combined with a lambda click callback, it's the cleanest full solution:

// DiffUtil callback — tells ListAdapter how to compare items
class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User) =
        oldItem.id == newItem.id  // same row?

    override fun areContentsTheSame(oldItem: User, newItem: User) =
        oldItem == newItem  // same data? (uses data class equals)
}
class UserListAdapter(
    private val onItemClick: (User) -> Unit,
    private val onLongClick: (User) -> Boolean = { false }
) : ListAdapter<User, UserListAdapter.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
            binding.root.setOnClickListener { onItemClick(user) }
            binding.root.setOnLongClickListener { onLongClick(user) }
        }
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) =
        holder.bind(getItem(position))  // getItem() from ListAdapter
}
// In Fragment — submit list updates, only changed rows animate
val adapter = UserListAdapter(
    onItemClick = { user -> navigateToDetail(user) },
    onLongClick = { user -> showOptions(user); true }
)
binding.recyclerView.adapter = adapter

// Observe and submit new list — DiffUtil handles the rest
viewModel.users.observe(viewLifecycleOwner) { users ->
    adapter.submitList(users)
}
✅ Use ListAdapter for new projects. It gives you smooth item animations for free, avoids full list redraws, and works perfectly with LiveData or StateFlow from your ViewModel.

Which Pattern Should You Use?

Pattern Best For Avoid When
Lambda in constructor Simple single action per item Multiple different click types needed
Interface callback Multiple click actions (click + long press + delete) Simple single action — overkill
Multiple zones Different parts of item do different things Simple list items with one action
ListAdapter + DiffUtil All new projects — always Never — always prefer over plain Adapter

Best Practices

  • Always pass the item to the callback, not the View — the old Java pattern passed a View and called getTag(). In Kotlin, pass the typed data object directly. No tags, no casting, compiler-safe.
  • Set click listeners in bind(), not onCreateViewHolder() — ViewHolders are reused for different items. If you set the listener in onCreateViewHolder(), it captures a stale item reference. Always set it in bind() where the current item is known.
  • Use ListAdapter over RecyclerView.AdapterListAdapter with DiffUtil gives you free smooth animations and prevents full list redraws. There's no reason to use plain RecyclerView.Adapter in new code.
  • Don't call notifyDataSetChanged() — it redraws every item regardless of what changed. Use submitList() with ListAdapter instead, which only redraws the rows that actually changed.
  • Use ripple effect on clickable items — add android:background="?attr/selectableItemBackground" to your item root view for the Material ripple. Without it, items feel unresponsive even when clicks work.

Frequently Asked Questions

How do I add a click listener to RecyclerView items in Kotlin?
Pass a lambda to your adapter constructor and call it in bind() using binding.root.setOnClickListener { onItemClick(item) }. This passes the typed item directly — no getTag(), no casting.

Why doesn't RecyclerView have setOnItemClickListener?
It was intentionally removed to give full control — you can set independent listeners on different parts of an item, handle long press, and pass typed data instead of raw Views. The flexibility is the feature.

What is the difference between RecyclerView.Adapter and ListAdapter?
ListAdapter adds automatic DiffUtil support. Call submitList() with a new list and it only animates changed rows in the background. Plain RecyclerView.Adapter requires manual notifyItem* calls. Always use ListAdapter for new projects.

How do I handle long click on RecyclerView items?
Use setOnLongClickListener on binding.root in bind(). Return true to consume the event, false to let it propagate. Pass the item to the callback so the caller knows which item was long-pressed.

📝 Summary
  • Lambda in constructor — simplest, best for one action per item
  • Interface callback — best when you need click + long click + child view clicks
  • Multiple zones — different parts of item trigger different actions
  • ListAdapter + DiffUtil — always use for new projects, free animations
  • Always set listeners in bind() — not in onCreateViewHolder()
  • Pass the typed item to callback — not the View
  • Add selectableItemBackground ripple to item root for tactile feedback
  • Never call notifyDataSetChanged() — use submitList() instead

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