Skip to content
intermediate Phase 3 · UI with XML & Views

RecyclerView

Display scrollable lists with RecyclerView, adapters, ViewHolders, and layout managers.

1h
4 problems
Topic Progress 0%

RecyclerView Fundamentals

Why RecyclerView Exists

ListView handled simple lists but recycled views poorly and had no built-in support for complex item layouts or horizontal scrolling. RecyclerView solves these problems by enforcing a strict separation of concerns: the RecyclerView manages view recycling, the LayoutManager decides positioning, and the Adapter supplies data.

The Three Pillars

  1. Adapter — binds data to views, creates ViewHolders
  2. ViewHolder — holds references to views for a single item; recycled as the user scrolls
  3. LayoutManager — decides how items are positioned (linear, grid, staggered)

Minimal Working Example

// Data class
data class Task(val id: Int, val title: String, val completed: Boolean)

// ViewHolder
class TaskViewHolder(
    private val binding: ItemTaskBinding
) : RecyclerView.ViewHolder(binding.root) {
    fun bind(task: Task) {
        binding.taskTitle.text = task.title
        binding.taskCheckbox.isChecked = task.completed
    }
}

// Adapter
class TaskAdapter(
    private val onClick: (Task) -> Unit
) : ListAdapter<Task, TaskViewHolder>(TaskDiffCallback()) {

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

    override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
        holder.bind(getItem(position))
        holder.itemView.setOnClickListener { onClick(getItem(position)) }
    }
}

// DiffUtil callback
class TaskDiffCallback : DiffUtil.ItemCallback<Task>() {
    override fun areItemsTheSame(old: Task, new: Task) = old.id == new.id
    override fun areContentsTheSame(old: Task, new: Task) = old == new
}

LayoutManager Options

LayoutManager Behavior Use Case
LinearLayoutManager Items in a single row (horizontal or vertical) Chat messages, horizontal carousels
GridLayoutManager(spanCount) Items in a grid Photo galleries, product listings
StaggeredGridLayoutManager Items in a grid with varying item heights Pinterest-style feeds

Setting Up in Activity/Fragment

val recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = TaskAdapter { task ->
    // Handle click
}

// Set fixed size if items don't change RecyclerView's bounds
recyclerView.setHasFixedSize(true)

Why setHasFixedSize(true)?

When the RecyclerView's size doesn't change (items scroll within a fixed container), setHasFixedSize(true) skips expensive requestLayout calls during adapter updates. Use it when the RecyclerView itself has a defined bounds, like match_parent inside a layout.

DiffUtil and Efficient List Updates

The Problem with notifyDataSetChanged()

Calling notifyDataSetChanged() tells RecyclerView that every single item might have changed. It re-binds every visible item and re-measures the entire layout. For a list with 50 visible items, this means 50+ bind calls even if only one item changed.

How DiffUtil Works

DiffUtil calculates the minimal set of insert, remove, move, and change operations to transform the old list into the new list. RecyclerView then animates only the affected items.

class ProductDiffCallback : DiffUtil.ItemCallback<Product>() {
    // Are these the same logical item (same ID)?
    override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean {
        return oldItem.id == newItem.id
    }

    // Same item but has the data changed?
    override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean {
        return oldItem == newItem  // data class equals check
    }

    // Optional: provide change payload for partial updates
    override fun getChangePayload(oldItem: Product, newItem: Product): Any? {
        if (oldItem.price != newItem.price) return PRICE_CHANGED
        if (oldItem.name != newItem.name) return NAME_CHANGED
        return null
    }
}

SubmitList Pattern

ListAdapter (which extends RecyclerView.Adapter) has a built-in submitList() method that runs DiffUtil on a background thread:

// In your Activity or ViewModel
adapter.submitList(newProducts)

Never mutate the list you pass to submitList(). Create a new list:

// WRONG — mutates in place, DiffUtil won't detect changes
products.add(newProduct)
adapter.submitList(products)

// RIGHT — creates a new list
val updated = products + newProduct
adapter.submitList(updated)

DiffUtil Performance

DiffUtil runs in O(N + D^2) time where N is the list size and D is the edit distance. For very large lists (10,000+ items), consider AsyncListDiffer to run the diff on a background thread without blocking the UI.

class ProductAdapter : RecyclerView.Adapter<ProductViewHolder>() {
    private val differ = AsyncListDiffer(this, ProductDiffCallback())

    fun submitList(list: List<Product>) = differ.submitList(list)

    override fun getItemCount() = differ.currentList.size
    // ...
}

Adapter Best Practices

  1. Never call notifyDataSetChanged() — use DiffUtil or targeted notify methods
  2. Bind data in onBindViewHolder, not in ViewHolder constructors
  3. Use ViewBinding in ViewHolder — avoids findViewById overhead on every bind
  4. Set click listeners in onBindViewHolder — ensures listeners match current data
  5. Use submitList() with immutable lists — always create a new list instance

Quiz

1. What is the primary responsibility of a RecyclerView.ViewHolder?

Question 1 options

2. In DiffUtil.ItemCallback, what does areContentsTheSame check?

Question 2 options

3. Why should you pass a new list to submitList instead of mutating the existing list?

Question 3 options

4. When should you use setHasFixedSize(true) on a RecyclerView?

Question 4 options

Flashcards

Question

What are the three pillars of RecyclerView?

Answer

Adapter (binds data to views), ViewHolder (holds recycled view references), and LayoutManager (positions items on screen).

Question

Why is notifyDataSetChanged() discouraged?

Answer

It tells RecyclerView every item changed, causing a full re-bind and re-measure of all visible items. Use DiffUtil or targeted notify methods instead.

Question

What is the time complexity of DiffUtil's calculation?

Answer

O(N + D^2) where N is the list size and D is the edit distance between old and new lists. For very large lists, use AsyncListDiffer.

Question

What LayoutManager should you use for a Pinterest-style feed with varying item heights?

Answer

StaggeredGridLayoutManager — it arranges items in a grid but allows each item to have a different height, creating the staggered masonry look.

Revision Notes

Key Takeaways

  • 1. RecyclerView recycles ViewHolder instances — never call findViewById in onBindViewHolder
  • 2. DiffUtil calculates minimal changes in O(N + D^2) time for efficient list updates
  • 3. Always pass a new list instance to submitList — mutating in place defeats DiffUtil
  • 4. Use ViewBinding in ViewHolders for type-safe, zero-cost view access
  • 5. setHasFixedSize(true) skips unnecessary layout passes when RecyclerView bounds are fixed

Interview Tips

  • Explain the ViewHolder pattern — view creation is expensive, recycling avoids repeated inflation
  • Describe DiffUtil's areItemsTheSame vs areContentsTheSame distinction clearly
  • Be ready to discuss when notifyDataSetChanged is acceptable (almost never)
  • Know the difference between ListAdapter and AsyncListDiffer for large lists

Cheat Sheet

RecyclerView Cheat Sheet

Setup:

recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = MyAdapter()
recyclerView.setHasFixedSize(true) // if bounds don't change

ViewHolder pattern:

  • ViewHolder holds ViewBinding references
  • Bind method receives data and sets view properties
  • RecyclerView recycles ViewHolder instances

DiffUtil rules:

  • areItemsTheSame → same entity (by ID)
  • areContentsTheSame → same data (by equals)
  • Always pass new list to submitList()
  • Never mutate the list in place

LayoutManagers:

  • LinearLayoutManager → single row/column
  • GridLayoutManager(n) → grid with n columns
  • StaggeredGridLayoutManager → variable-height grid