Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
📋 Screens, Lists & Navigation  ·  Lesson 23

RecyclerView Click, Grid & DiffUtil

Item clicks with a lambda

Kotlin
class SubjectAdapter(
    private val items: List<Subject>,
    private val onClick: (Subject) -> Unit            // the screen decides what a click does
) : RecyclerView.Adapter<SubjectAdapter.VH>() {

    class VH(val binding: ItemSubjectRowBinding) : RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: VH, position: Int) {
        val s = items[position]
        holder.binding.tvName.text = s.name
        holder.binding.root.setOnClickListener { onClick(s) }
    }
    override fun getItemCount() = items.size
}

// Activity
binding.recyclerView.adapter = SubjectAdapter(subjects) { subject ->
    startActivity(SubjectActivity.newIntent(this, subject.name))
}

ListAdapter + DiffUtil

ListAdapter stores the list for you and uses DiffUtil on a background thread to work out what changed. You just call submitList().

Kotlin – SubjectListAdapter.kt
class SubjectListAdapter(private val onClick: (Subject) -> Unit) :
    ListAdapter<Subject, SubjectListAdapter.VH>(DIFF) {     // no list field – ListAdapter keeps it

    class VH(val binding: ItemSubjectRowBinding) : RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: VH, position: Int) {
        val s = getItem(position)
        holder.binding.tvName.text = s.name
        holder.binding.root.setOnClickListener { onClick(s) }
    }

    companion object {
        val DIFF = object : DiffUtil.ItemCallback<Subject>() {
            override fun areItemsTheSame(old: Subject, new: Subject) = old.name == new.name   // same row?
            override fun areContentsTheSame(old: Subject, new: Subject) = old == new          // same data?
        }
    }
}

// usage
val adapter = SubjectListAdapter { openSubject(it) }
binding.recyclerView.adapter = adapter
adapter.submitList(subjects)                      // later: adapter.submitList(newList)

Old list

  • Maths
  • Physics
  • Chemistry

New list

  • Maths
  • Physics ✎ (changed)
  • Biology ✚ (inserted)
DiffUtil: keep Maths, rebind Physics, remove Chemistry, insert Biology — with animations

Real adapter from Student Diary

The note list in the final project shows title, details, subject chip, due date, priority strip, done checkbox, favourite star and a photo thumbnail:

Kotlin – ui/notes/NoteAdapter.kt
package com.codingeasily.studentdiary.ui.notes

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.data.Note
import com.codingeasily.studentdiary.databinding.ItemNoteBinding
import com.codingeasily.studentdiary.util.DateUtils
import java.io.File

/**
 * ListAdapter + DiffUtil: when the list changes, only the changed rows are redrawn.
 */
class NoteAdapter(
    private val onClick: (Note) -> Unit,
    private val onToggleDone: (Note) -> Unit,
    private val onToggleFavorite: (Note) -> Unit
) : ListAdapter<Note, NoteAdapter.NoteViewHolder>(DIFF) {

    inner class NoteViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: NoteViewHolder, position: Int) {
        val note = getItem(position)
        val b = holder.binding
        val ctx = b.root.context

        b.tvTitle.text = note.title
        b.tvTitle.alpha = if (note.isDone) 0.5f else 1f
        b.tvSubject.text = note.subject
        b.tvContent.text = note.content
        b.tvContent.isVisible = note.content.isNotBlank()
        b.tvDue.isVisible = note.dueAt != null
        note.dueAt?.let { b.tvDue.text = ctx.getString(R.string.due_at, DateUtils.formatShort(it)) }

        val color = when (note.priority) {
            Note.PRIORITY_HIGH -> R.color.priority_high
            Note.PRIORITY_LOW -> R.color.priority_low
            else -> R.color.priority_medium
        }
        b.priorityStrip.setBackgroundColor(ContextCompat.getColor(ctx, color))

        // Remove the old listener first, otherwise setChecked() would fire it while recycling
        b.cbDone.setOnCheckedChangeListener(null)
        b.cbDone.isChecked = note.isDone
        b.cbDone.setOnCheckedChangeListener { _, _ -> onToggleDone(note) }

        b.btnFavorite.setImageResource(if (note.isFavorite) R.drawable.ic_star else R.drawable.ic_star_outline)
        b.btnFavorite.setOnClickListener { onToggleFavorite(note) }

        b.ivThumb.isVisible = note.imagePath != null
        note.imagePath?.let { Glide.with(b.ivThumb).load(File(it)).centerCrop().into(b.ivThumb) }

        b.root.setOnClickListener { onClick(note) }
    }

    companion object {
        private val DIFF = object : DiffUtil.ItemCallback<Note>() {
            override fun areItemsTheSame(old: Note, new: Note) = old.id == new.id
            override fun areContentsTheSame(old: Note, new: Note) = old == new
        }
    }
}
9:41▮▮ 85%☰Student Diary⋮AllPendingDone☐ Maths homework ★Maths · Due 24 Nov☐ Physics revision ☆Physics☑ Buy notebook ☆General+
Each row = item_note.xml bound by NoteAdapter

Grid and horizontal lists

Kotlin
// 2 columns (Student Diary – SubjectsFragment)
binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)

// horizontal carousel
binding.rvBanners.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
PagerSnapHelper().attachToRecyclerView(binding.rvBanners)   // snap one card at a time

// header that spans both columns in a grid
(binding.recyclerView.layoutManager as GridLayoutManager).spanSizeLookup =
    object : GridLayoutManager.SpanSizeLookup() {
        override fun getSpanSize(position: Int) = if (position == 0) 2 else 1
    }
9:41▮▮ 85%Subjects⋮🛍️Maths5 notes🛍️Physics3 notes🛍️Chemistry2 notes🛍️English4 notes🛍️Computer6 notes🛍️Hindi1 note
GridLayoutManager(context, 2)

Swipe to delete + Undo

Kotlin
// From Student Diary – NoteListFragment
val swipe = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
    override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
    override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) {
        val position = vh.bindingAdapterPosition
        if (position == RecyclerView.NO_POSITION) return
        val note = adapter.currentList[position]
        viewModel.delete(note)
        Snackbar.make(binding.root, getString(R.string.note_deleted, note.title), Snackbar.LENGTH_LONG)
            .setAction(R.string.undo) { viewModel.restore(note) }
            .show()
    }
}
ItemTouchHelper(swipe).attachToRecyclerView(binding.recyclerView)
Swipe rowonSwipedpositionviewModel.deleteRoom DELETESnackbarUNDO → restore
The list updates itself because Room emits a new list

Empty state

Kotlin
viewModel.notes(filter).observe(viewLifecycleOwner) { list ->
    adapter.submitList(list)
    binding.emptyView.isVisible = list.isEmpty()     // "No notes here yet. Tap + to add one."
}

Multiple view types

Kotlin – headers + items
sealed class Row {
    data class Header(val title: String) : Row()
    data class Item(val note: Note) : Row()
}

override fun getItemViewType(position: Int) = when (getItem(position)) {
    is Row.Header -> TYPE_HEADER
    is Row.Item -> TYPE_ITEM
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    return if (viewType == TYPE_HEADER) HeaderVH(ItemHeaderBinding.inflate(inflater, parent, false))
           else NoteVH(ItemNoteBinding.inflate(inflater, parent, false))
}

Common mistakes

  • Submitting the same mutable list after editing it → no update. Submit a new list.
  • areItemsTheSame comparing whole objects → every change looks like remove + insert (flicker). Compare ids.
  • CheckBox listener firing while binding → set the listener to null first, then set isChecked, then the new listener (see NoteAdapter).
  • Using adapterPosition (deprecated) → use bindingAdapterPosition.

Summary

  • Pass click actions as lambdas into the adapter.
  • ListAdapter + DiffUtil + submitList = efficient, animated updates.
  • GridLayoutManager for grids; ItemTouchHelper for swipe; show an empty view when the list is empty.
✅ Practice

Convert your states list to ListAdapter, add a search box that submits a filtered list, show it as a 2-column grid on tablets (resources.configuration.screenWidthDp >= 600) and add swipe-to-delete with Undo.

Frequently Asked Questions

What does DiffUtil do?
It compares the old and new list, finds which items were added, removed, moved or changed, and updates only those rows with animations.
Why does submitList not update my list?
If you pass the same mutable list object after changing it, DiffUtil sees no difference. Always submit a new list (for example list.toList()) — Room and Flow do this automatically.
Where should I set the click listener?
In onBindViewHolder (simple) or in the ViewHolder init block (slightly faster). Pass the action into the adapter as a lambda so the adapter does not know about navigation.
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicFragments & Fragment Lifecycle Your progress: 23 / 43 (53%) · Lesson 24 / 43 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free