androidengineers.Book a session

Performance Optimization

RecyclerView: DiffUtil, Prefetch, Pools

article25 minHard

RecyclerView is the most performance-critical UI component in most Android apps. Understanding DiffUtil, prefetching, and view pools unlocks significant scroll performance improvements.

DiffUtil: Efficient List Updates

Never call notifyDataSetChanged() — it rebuilds the entire list, drops animations, and causes visual flicker. Use DiffUtil.Callback or ListAdapter instead.

ListAdapter (Recommended)

class ArticleAdapter : ListAdapter<Article, ArticleViewHolder>(DIFF_CALLBACK) {

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

    override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    companion object {
        val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Article>() {
            // Called to decide if two items represent the same identity
            override fun areItemsTheSame(old: Article, new: Article) = old.id == new.id

            // Called when areItemsTheSame returns true — decides if content changed
            override fun areContentsTheSame(old: Article, new: Article) = old == new

            // Optional: return the payload of what changed for partial binding
            override fun getChangePayload(old: Article, new: Article): Any? {
                return if (old.likeCount != new.likeCount) "like_count" else null
            }
        }
    }
}

// In ViewModel/Fragment — submitList triggers DiffUtil on a background thread
adapter.submitList(newArticles)

Payload-Based Partial Bind

override fun onBindViewHolder(holder: ArticleViewHolder, position: Int, payloads: List<Any>) {
    if (payloads.isEmpty()) {
        holder.bind(getItem(position))  // full bind
    } else {
        // Only update what changed — avoids image reload/flicker
        payloads.forEach { payload ->
            when (payload) {
                "like_count" -> holder.bindLikeCount(getItem(position).likeCount)
            }
        }
    }
}

RecyclerView Prefetch

By default, RecyclerView prefetches items during scroll fling. Tune when items are complex:

val layoutManager = LinearLayoutManager(context)
layoutManager.initialPrefetchItemCount = 5  // prefetch 5 items ahead

// For GridLayoutManager:
val gridLayoutManager = GridLayoutManager(context, 3)
gridLayoutManager.initialPrefetchItemCount = 6  // 2 rows × 3 columns

RecycledViewPool: Sharing Pools

When you have nested RecyclerViews (a horizontal list inside a vertical list), sharing a RecycledViewPool prevents redundant ViewHolder creation:

val sharedPool = RecyclerView.RecycledViewPool()
sharedPool.setMaxRecycledViews(R.layout.item_article, 10)  // keep 10 article views in pool

// Apply to each inner RecyclerView
class OuterAdapter : RecyclerView.Adapter<OuterViewHolder>() {
    override fun onBindViewHolder(holder: OuterViewHolder, position: Int) {
        holder.innerRecyclerView.setRecycledViewPool(sharedPool)
        // set layoutManager.initialPrefetchItemCount too
    }
}

ViewHolder Best Practices

class ArticleViewHolder(private val binding: ItemArticleBinding) :
    RecyclerView.ViewHolder(binding.root) {

    // ✅ Set click listener once in constructor — not in bind()
    private var currentArticle: Article? = null
    var onClickListener: ((Article) -> Unit)? = null

    init {
        binding.root.setOnClickListener {
            currentArticle?.let { onClickListener?.invoke(it) }
        }
    }

    fun bind(article: Article) {
        currentArticle = article
        binding.titleText.text = article.title
        // Use Coil/Glide — handles cancel/recycle automatically
        binding.thumbnailImage.load(article.thumbnailUrl) {
            crossfade(true)
            placeholder(R.drawable.placeholder)
        }
    }

    fun bindLikeCount(count: Int) {
        binding.likeCount.text = count.toString()
    }
}

Common Performance Mistakes

MistakeImpactFix
notifyDataSetChanged()Full rebind, no animationsUse submitList() with ListAdapter
Decoding bitmaps in onBindViewHolderJank on every scrollUse Glide/Coil; never decode on main thread
Creating click listeners in bind()Allocation on every bindMove to init block
wrap_content RecyclerView in ScrollViewDouble measure passUse NestedScrollView or fixed height
No setHasStableIds(true)Item change animations disabledSet if IDs are stable and unique

Key Takeaways

  • ListAdapter + DiffUtil.ItemCallback is the correct, modern adapter pattern
  • areItemsTheSame checks identity (ID); areContentsTheSame checks equality (fields)
  • Use getChangePayload + payload-aware onBindViewHolder for targeted UI updates
  • Share RecycledViewPool across nested RecyclerViews in the same screen
  • Set initialPrefetchItemCount to match visible item count for smooth flings

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
RecyclerView: DiffUtil, Prefetch, Pools | Android System Design | Android Engineers