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
| Mistake | Impact | Fix |
|---|---|---|
notifyDataSetChanged() | Full rebind, no animations | Use submitList() with ListAdapter |
Decoding bitmaps in onBindViewHolder | Jank on every scroll | Use Glide/Coil; never decode on main thread |
Creating click listeners in bind() | Allocation on every bind | Move to init block |
wrap_content RecyclerView in ScrollView | Double measure pass | Use NestedScrollView or fixed height |
No setHasStableIds(true) | Item change animations disabled | Set if IDs are stable and unique |
Key Takeaways
ListAdapter+DiffUtil.ItemCallbackis the correct, modern adapter patternareItemsTheSamechecks identity (ID);areContentsTheSamechecks equality (fields)- Use
getChangePayload+ payload-awareonBindViewHolderfor targeted UI updates - Share
RecycledViewPoolacross nested RecyclerViews in the same screen - Set
initialPrefetchItemCountto match visible item count for smooth flings