RecyclerView, ListAdapter and DiffUtil

Views, layouts and RecyclerView Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Spot the bug

After toggling a favourite, the row doesn't update. Which line causes it?

  1. private val items = mutableListOf<Item>()
  2. ​
  3. fun toggle(id: Long) {
  4. val i = items.indexOfFirst { it.id == id }
  5. items[i] = items[i].copy(favourite = !items[i].favourite)
  6. adapter.submitList(items)
  7. }
Hint 1 of 1

Is the submitted list a new object?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 6 adapter.submitList(items)

The same list instance is submitted again, so ListAdapter sees the same reference and skips the diff. Submit a new list (items.toList(), or currentList.map { ... }).

30-second answer

RecyclerView creates only enough ViewHolders to fill the screen plus a few, and rebinds them with new data as you scroll. ListAdapter with a DiffUtil.ItemCallback compares the old and new list on a background thread and dispatches exact insert, remove, move and change events, so only affected rows rebind and animations work. notifyDataSetChanged() rebinds everything and kills animations.

Practise saying it · 35 s. No recording, just a timer

Explain RecyclerView recycling and DiffUtil.

Say your answer out loud, then check which of these points you covered:

  • ViewHolders are reused
  • DiffUtil computes minimal changes
  • areItemsTheSame vs areContentsTheSame
  • Submit a new list

Deep dive

areItemsTheSame compares IDs; areContentsTheSame compares contents.

submitList must get a new list instance; submitting the same mutated list does nothing.

Use setHasStableIds only with real stable IDs.

Shared RecycledViewPool helps nested horizontal lists reuse views.

ListAdapter with DiffUtil Compile-checked · Kotlin 2.4.21
data class Item(val id: Long, val title: String, val favourite: Boolean)

object ItemDiff : DiffUtil.ItemCallback<Item>() {
    override fun areItemsTheSame(old: Item, new: Item) = old.id == new.id
    override fun areContentsTheSame(old: Item, new: Item) = old == new
}

class ItemAdapter : ListAdapter<Item, ItemAdapter.Holder>(ItemDiff) {
    class Holder(val text: TextView) : RecyclerView.ViewHolder(text)
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(TextView(parent.context))
    override fun onBindViewHolder(holder: Holder, position: Int) {
        holder.text.text = getItem(position).title
    }
}

Common mistakes

  • Calling notifyDataSetChanged on every update.
  • Mutating the current list and calling submitList with it.
  • Comparing whole objects in areItemsTheSame.

Senior take & follow-ups

Explain the two callbacks precisely and the same-instance trap.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why does submitList with the same list do nothing?

    Show hint

    ListAdapter sees the same reference and skips diffing.

  2. What's a payload?

    Show hint

    Partial change info passed to onBindViewHolder to update part of a row.

Compare on iOS: UICollectionView vs UITableView and diffable data sources Diffable data sources do the same diffing for UIKit lists.

Suggest a fix if something here is wrong or out of date.