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
- Adapter — binds data to views, creates ViewHolders
- ViewHolder — holds references to views for a single item; recycled as the user scrolls
- 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
- Never call
notifyDataSetChanged()— use DiffUtil or targeted notify methods - Bind data in
onBindViewHolder, not in ViewHolder constructors - Use ViewBinding in ViewHolder — avoids
findViewByIdoverhead on every bind - Set click listeners in
onBindViewHolder— ensures listeners match current data - Use
submitList()with immutable lists — always create a new list instance
Quiz
1. What is the primary responsibility of a RecyclerView.ViewHolder?
2. In DiffUtil.ItemCallback, what does areContentsTheSame check?
3. Why should you pass a new list to submitList instead of mutating the existing list?
4. When should you use setHasFixedSize(true) on a RecyclerView?
Flashcards
Question
What are the three pillars of RecyclerView?
Click to reveal answer
Answer
Adapter (binds data to views), ViewHolder (holds recycled view references), and LayoutManager (positions items on screen).
Question
Why is notifyDataSetChanged() discouraged?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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