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

ComponentResponsibility
RecyclerViewDisplays and recycles item views
AdapterConnects data with item views
ViewHolderHolds references to one item’s views
LayoutManagerDecides item arrangement
DiffUtilCalculates changes between two lists
ListAdapterApplies 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 --> Visible

RecyclerView 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.

LayoutManagerUse case
LinearLayoutManagerVertical or horizontal list
GridLayoutManagerGrid with fixed columns
StaggeredGridLayoutManagerItems 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()`
DiffUtilnotifyDataSetChanged()
Updates only changed itemsRefreshes all visible items
Better performanceCan perform unnecessary binding
Supports animationsLimited change information
Detects insert/remove/moveDoes 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 --> UI

Manual 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:

ComponentResponsibility
PagingSourceLoads pages from one source
PagerCreates the paging stream
PagingDataRepresents paged data
PagingDataAdapterDisplays PagingData
RemoteMediatorCoordinates API and Room
LoadStateRepresents 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 = 4

Benefit

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

MistakeBetter approach
Calling notifyDataSetChanged()Use ListAdapter and DiffUtil
Creating listeners during every bindReuse listeners or keep binding lightweight
Running API/database calls in AdapterPerform them through ViewModel/repository
Loading full-size imagesRequest display-sized images
Using position as item IDUse a stable domain ID
Not resetting recycled stateSet every item state during binding
Mutating the submitted listSubmit a new immutable list
Creating child Adapter every bindReuse Adapter and update its data
Deeply nested item layoutsUse a flatter layout
Rebinding full rowsUse DiffUtil payloads
Loading all records togetherUse pagination
Aggressive prefetchingPrefetch 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.