RecyclerView & List Performance
RecyclerView & List Performance
1. What is RecyclerView?
RecyclerView displays large or dynamic lists efficiently by reusing item views that move off-screen.
Common examples:
* Product list
* Transaction history
* Chat messages
* News feed
* Horizontal category list
flowchart LR
Data["Data list"] --> Adapter
Adapter --> ViewHolder
ViewHolder --> RecyclerView
RecyclerView --> Screen["Visible items"]---
2. Main Components
| Component | Responsibility |
|---|---|
| RecyclerView | Displays and recycles item views |
| Adapter | Connects data with item views |
| ViewHolder | Holds references to one item’s views |
| LayoutManager | Decides item arrangement |
| DiffUtil | Calculates changes between two lists |
| ListAdapter | Applies DiffUtil automatically |
---
3. Adapter
An Adapter performs three primary operations.
## `onCreateViewHolder()`Creates a new item ViewHolder when a reusable one is unavailable.
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return UserViewHolder(binding)
}## `onBindViewHolder()`Binds data to a new or recycled ViewHolder.
override fun onBindViewHolder(
holder: UserViewHolder,
position: Int
) {
holder.bind(getItem(position))
}## `getItemCount()`Returns the number of items.With ListAdapter, it is implemented internally.
---
4. ViewHolder
A ViewHolder holds the views of one list item.
class UserViewHolder(
private val binding: ItemUserBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(user: User) {
binding.name.text = user.name
binding.email.text = user.email
}
}Why use ViewHolder?
Without ViewHolder, Android would repeatedly search for views while scrolling.
Benefits:
* Reduces repeated view lookup
* Improves scrolling performance
* Keeps binding logic organized
* Supports view recycling
---
5. How Recycling Works
Suppose the screen can display six items:
1. RecyclerView creates ViewHolders for visible items.
2. The user scrolls.
3. An item moves off-screen.
4. Its ViewHolder enters the recycle pool.
5. RecyclerView reuses it for a new item.
6. Adapter binds new data to the reused ViewHolder.
flowchart LR
Visible["Visible ViewHolder"] --> Offscreen["Moves off-screen"]
Offscreen --> Pool["Recycle pool"]
Pool --> Rebind["Bind new data"]
Rebind --> VisibleRecyclerView does not create a new View for every data item.
Important binding rule
Always reset every state that might have been changed previously.
fun bind(user: User) {
binding.name.text = user.name
binding.badge.isVisible = user.isPremium
binding.root.isSelected = user.isSelected
}If `isVisible` is updated only for premium users, a recycled item may incorrectly show an old badge.---
6. Layout Managers
A LayoutManager controls how list items are arranged.
| LayoutManager | Use case |
|---|---|
LinearLayoutManager | Vertical or horizontal list |
GridLayoutManager | Grid with fixed columns |
StaggeredGridLayoutManager | Items with different sizes |
recyclerView.layoutManager = LinearLayoutManager(context)recyclerView.layoutManager = GridLayoutManager(context, 2)Interview question: RecyclerView vs ListView
RecyclerView provides:
* Mandatory ViewHolder pattern
* Flexible LayoutManagers
* Better animations
* DiffUtil support
* Item decorations
* More recycling control
* Easy support for different item types
---
7. DiffUtil
DiffUtil compares an old list with a new list and calculates the minimum required updates.
object UserDiffCallback : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(
oldItem: User,
newItem: User
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: User,
newItem: User
): Boolean {
return oldItem == newItem
}
}Important difference
### `areItemsTheSame()`Checks whether both objects represent the same logical item.
oldItem.id == newItem.id### `areContentsTheSame()`Checks whether the item’s displayed data has changed.
oldItem == newItem## DiffUtil vs `notifyDataSetChanged()`| DiffUtil | notifyDataSetChanged() |
|---|---|
| Updates only changed items | Refreshes all visible items |
| Better performance | Can perform unnecessary binding |
| Supports animations | Limited change information |
| Detects insert/remove/move | Does not provide exact differences |
---
8. ListAdapter
ListAdapter combines RecyclerView Adapter with asynchronous DiffUtil processing.
class UserAdapter(
private val onClick: (User) -> Unit
) : ListAdapter<User, UserViewHolder>(UserDiffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
val binding = ItemUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return UserViewHolder(binding)
}
override fun onBindViewHolder(
holder: UserViewHolder,
position: Int
) {
holder.bind(getItem(position), onClick)
}
}Submit a list using:
adapter.submitList(users)Why is ListAdapter preferred?
* Calculates differences automatically
* Runs list comparison away from the main thread
* Reduces boilerplate
* Supports list animations
* Prevents unnecessary full-list refreshes
Important mistake
Do not modify and submit the same mutable list instance.
adapter.submitList(oldList + newUser)Prefer immutable list updates so DiffUtil can detect changes correctly.
---
9. Stable IDs
Stable IDs provide a unique and consistent identity for every item.
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return getItem(position).id
}Benefits
* Better item identity tracking
* Improved animations
* Better state preservation
* Useful when items move between positions
Requirements
An ID must:
* Be unique
* Remain unchanged for the same item
* Not depend on the current list position
Do not use this:
override fun getItemId(position: Int) = position.toLong()Positions change when items are inserted, removed or reordered.
Stable IDs complement DiffUtil; they do not replace it.---
10. Avoiding Unnecessary Binding
Problem
A complete item is rebound even when only one property changes.
Example: Only download progress changed, but image, title and buttons are also rebound.Solution: Payloads
override fun getChangePayload(
oldItem: Download,
newItem: Download
): Any? {
return if (oldItem.progress != newItem.progress) {
newItem.progress
} else {
null
}
}override fun onBindViewHolder(
holder: DownloadViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNotEmpty()) {
holder.updateProgress(payloads.last() as Int)
} else {
super.onBindViewHolder(holder, position, payloads)
}
}Additional optimization rules
* Do not perform database or network calls in onBindViewHolder().
* Do not create expensive objects during every bind.
* Avoid repeatedly formatting unchanged values.
* Resize images to their display size.
* Avoid deeply nested item layouts.
* Use immutable item models.
* Update only affected items.
* Move complex calculations outside the Adapter.
---
11. Click Handling
Use the current ViewHolder position, not the old bound position.
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onClick(getItem(position))
}
}An even cleaner approach is to bind the actual object:
fun bind(user: User, onClick: (User) -> Unit) {
binding.name.text = user.name
binding.root.setOnClickListener {
onClick(user)
}
}Avoid storing Activity or Fragment references inside the Adapter.
---
12. Pagination
Pagination loads data in smaller pages instead of loading the complete dataset.
flowchart LR
UI["RecyclerView"] --> NearEnd["User reaches end"]
NearEnd --> Load["Load next page"]
Load --> Append["Append items"]
Append --> UIManual pagination
A scroll listener detects when the user approaches the end and requests another page.
The implementation must manage:
* Current page
* Loading state
* End-of-list state
* Duplicate-request prevention
* Retry
* Error state
* Configuration changes
Paging 3
Paging 3 is preferred for large or remote datasets.
Main components:
| Component | Responsibility |
|---|---|
PagingSource | Loads pages from one source |
Pager | Creates the paging stream |
PagingData | Represents paged data |
PagingDataAdapter | Displays PagingData |
RemoteMediator | Coordinates API and Room |
LoadState | Represents loading and errors |
val users = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = {
userRepository.getPagingSource()
}
).flow.cachedIn(viewModelScope)Why use cachedIn(viewModelScope)?
It keeps paging data available across configuration changes and prevents unnecessary reloads.
Important pagination mistakes
* Triggering multiple next-page requests
* Resetting data during rotation
* No end-of-list handling
* No retry mechanism
* Loading pages directly from Adapter
* Not handling duplicate records
* Using position as item identity
---
13. Prefetching
Prefetching prepares data or Views before they become visible.
RecyclerView’s LayoutManager can prefetch nearby items during scrolling.
For nested horizontal lists:
val layoutManager = LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
false
)
layoutManager.initialPrefetchItemCount = 4Benefit
The upcoming items may be measured and prepared before the user sees them, producing smoother scrolling.
Risk
Excessive prefetching can increase:
* CPU work
* Memory usage
* View creation
* Image requests
Prefetch only a small number of likely-to-be-visible items.
---
14. Nested RecyclerViews
A common example is a vertical home feed containing horizontal product lists.
Common problems
* Repeated ViewHolder creation
* Lost horizontal scroll position
* Increased measurement cost
* Too many image requests
* Poor recycling
* Nested scrolling conflicts
Share a RecycledViewPool
private val sharedPool = RecyclerView.RecycledViewPool()
fun bind(section: Section) {
binding.childRecyclerView.apply {
setRecycledViewPool(sharedPool)
isNestedScrollingEnabled = false
}
}Preserve child scroll position
Do not create a new child Adapter or LayoutManager on every parent bind. Preserve state using stable section IDs or managed LayoutManager state.
When should nested lists be avoided?
Avoid them when the same UI can be represented efficiently using:
* Multiple view types in one RecyclerView
* ConcatAdapter
* A flattened list model
* GridLayoutManager with span sizes
---
15. ConcatAdapter
ConcatAdapter combines multiple Adapters into one RecyclerView.
recyclerView.adapter = ConcatAdapter(
headerAdapter,
categoryAdapter,
productAdapter,
footerAdapter
)It is useful for:
* Headers
* Content sections
* Loading rows
* Footer actions
* Separately managed list sections
It can be more efficient and simpler than unnecessary nested RecyclerViews.
---
16. Common Performance Mistakes
| Mistake | Better approach |
|---|---|
Calling notifyDataSetChanged() | Use ListAdapter and DiffUtil |
| Creating listeners during every bind | Reuse listeners or keep binding lightweight |
| Running API/database calls in Adapter | Perform them through ViewModel/repository |
| Loading full-size images | Request display-sized images |
| Using position as item ID | Use a stable domain ID |
| Not resetting recycled state | Set every item state during binding |
| Mutating the submitted list | Submit a new immutable list |
| Creating child Adapter every bind | Reuse Adapter and update its data |
| Deeply nested item layouts | Use a flatter layout |
| Rebinding full rows | Use DiffUtil payloads |
| Loading all records together | Use pagination |
| Aggressive prefetching | Prefetch only nearby items |
---
Important Interview Questions and Answers
Q1. How does RecyclerView improve performance?
It creates Views only for visible and nearby items, then recycles off-screen ViewHolders for new positions.
Q2. RecyclerView vs ListView?
RecyclerView provides mandatory ViewHolder usage, LayoutManagers, item animations, DiffUtil and better customization.
Q3. What is the difference between Adapter and ViewHolder?
Adapter connects the complete dataset to RecyclerView. ViewHolder manages the View for one item.
Q4. Why should every state be reset during binding?
A ViewHolder may contain state from a previously displayed item because it is recycled.
Q5. DiffUtil vs ListAdapter?
DiffUtil calculates list changes. ListAdapter is an Adapter implementation that uses DiffUtil automatically.
Q6. Why is notifyDataSetChanged() inefficient?
It does not describe exact changes, so RecyclerView may rebind all visible items unnecessarily.
Q7. What are stable IDs?
Unique IDs that remain unchanged for the same logical item, even when its position changes.
Q8. Do stable IDs replace DiffUtil?
No. Stable IDs help track identity; DiffUtil calculates content and structural changes.Q9. How do you avoid complete item rebinding?
Use DiffUtil payloads to update only the changed View.
Q10. Why should list positions not be captured permanently?
Insertions, removals and DiffUtil updates can change positions after binding.
Q11. How do you prevent duplicate pagination requests?
Maintain an isLoading flag, check end-of-list state and let the ViewModel or Paging library control requests.
Q12. What is RemoteMediator?
It coordinates network pagination with a Room database, usually making Room the UI’s source of truth.
Q13. What is prefetching?
It prepares upcoming item Views or data before they become visible.
Q14. How do you optimize nested RecyclerViews?
Share a RecycledViewPool, reuse child Adapters, control prefetching and preserve child scroll state.
Q15. How would you debug a slow RecyclerView?
Profile frame time, measure binding cost, inspect layout depth, check image sizes, find repeated View creation and identify unnecessary updates.
Interview-ready summary
> RecyclerView displays large lists efficiently using ViewHolder recycling. I prefer ListAdapter with DiffUtil, stable domain IDs and immutable list updates. I keep binding lightweight, reset all recycled state and use payloads for partial updates. For large datasets, I use Paging 3. For nested lists, I reuse child adapters, share a RecycledViewPool and keep prefetching controlled.