Adapters & DiffUtil
Build efficient, animated list adapters with ListAdapter and DiffUtil.ItemCallback. Use submitList() for smooth, partial updates.
Adapters & DiffUtil is a free Android Academy lesson on CoddyKit. This is lesson 6 of 6. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, and your progress syncs across the web and the CoddyKit app. The Android Academy course includes 6 lessons in total.
The Problem with notifyDataSetChanged()
When your RecyclerView data changes, calling notifyDataSetChanged() redraws every item — even unchanged ones. This causes:
- No item animations
- Poor performance on large lists
- Flickering UI
DiffUtil calculates the minimum set of changes and applies them with smooth animations.
DiffUtil.ItemCallback
Define how two items are compared. Two questions to answer:
areItemsTheSame— do they represent the same logical item? (usually compare IDs)areContentsTheSame— are all the visible fields identical?
data class Post(val id: Int, val title: String, val likes: Int)
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem.id == newItem.id // same post?
}
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem == newItem // data class equality (all fields)
}
}ListAdapter
ListAdapter is a RecyclerView.Adapter with DiffUtil built in. Pass your ItemCallback to its constructor and call submitList() to update data:
class PostAdapter : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
inner class ViewHolder(val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position) // use getItem(), not a manual list
holder.binding.tvTitle.text = post.title
holder.binding.tvLikes.text = "${post.likes} likes"
}
}Submitting New Lists
Call submitList() whenever your data changes. ListAdapter diffs the old and new list on a background thread and applies only the necessary changes:
// In Activity or Fragment:
val adapter = PostAdapter()
binding.recyclerView.adapter = adapter
// Observe ViewModel LiveData:
viewModel.posts.observe(viewLifecycleOwner) { posts ->
adapter.submitList(posts) // diff calculated off main thread
}
// Important: always submit a NEW list, not a mutated old one
// WRONG: currentList.add(newPost); adapter.submitList(currentList)
// RIGHT: adapter.submitList(currentList + newPost)Click Listeners in ListAdapter
Pass click handlers via the constructor — a common and clean pattern:
class PostAdapter(
private val onLike: (Post) -> Unit,
private val onOpen: (Post) -> Unit
) : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position)
holder.binding.tvTitle.text = post.title
holder.binding.btnLike.setOnClickListener { onLike(post) }
holder.itemView.setOnClickListener { onOpen(post) }
}
}
// Usage:
val adapter = PostAdapter(
onLike = { viewModel.likePost(it) },
onOpen = { navigateToDetail(it.id) }
)getChangePayload() — Partial Updates
When only one field changes (e.g., likes count), avoid full rebind with getChangePayload():
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Post, newItem: Post) = oldItem == newItem
override fun getChangePayload(oldItem: Post, newItem: Post): Any? {
// Return non-null payload to trigger partial bind
return if (oldItem.likes != newItem.likes) "likes_changed" else null
}
}
// In Adapter.onBindViewHolder with payloads:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any>) {
if (payloads.contains("likes_changed")) {
holder.binding.tvLikes.text = "${getItem(position).likes} likes"
} else {
super.onBindViewHolder(holder, position, payloads)
}
}Multiple View Types
Use getItemViewType() to render different item layouts in the same list:
class FeedAdapter : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {
companion object {
const val TYPE_POST = 0
const val TYPE_AD = 1
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is FeedItem.Post -> TYPE_POST
is FeedItem.Ad -> TYPE_AD
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
TYPE_POST -> PostViewHolder(ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
else -> AdViewHolder(ItemAdBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
}ConcatAdapter — Composing Lists
ConcatAdapter combines multiple adapters into a single RecyclerView — great for headers, footers, and loading states:
// Separate adapters for different sections
val headerAdapter = HeaderAdapter()
val postAdapter = PostAdapter()
val loaderAdapter = LoadingAdapter()
// Combine them in order
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, postAdapter, loaderAdapter)
// Update each independently
postAdapter.submitList(posts)
loaderAdapter.showLoading(true)AsyncListDiffer — Manual Control
If you can't extend ListAdapter, use AsyncListDiffer directly:
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private val differ = AsyncListDiffer(this, PostDiffCallback())
fun submitList(list: List<Post>) = differ.submitList(list)
fun getItem(position: Int): Post = differ.currentList[position]
override fun getItemCount() = differ.currentList.size
}Performance Tips
Keep your RecyclerView fast:
- Use
setHasStableIds(true)if your items have unique, stable IDs - Avoid creating objects inside
onBindViewHolder - Use
RecycledViewPoolto share recycled views across multiple RecyclerViews - Use
setItemViewCacheSize()to increase the off-screen cache
Animations Come Free
ListAdapter's DiffUtil integration automatically triggers DefaultItemAnimator for:
- Fade-in for new items
- Fade-out for removed items
- Move animation when an item changes position
Set a custom animator: recyclerView.itemAnimator = MyAnimator(). Set to null to disable all animations.
Quick Check
Which two methods must you implement in DiffUtil.ItemCallback?
Recap: Adapters & DiffUtil
Efficient, animated lists with minimal code:
DiffUtil.ItemCallback— define identity (areItemsTheSame) and equality (areContentsTheSame)ListAdapter— built-in async diffing; callsubmitList()- Always submit a new list reference, never mutate the current list
getChangePayload()for partial rebindsConcatAdapterto compose multiple adapters
Next: multi-screen navigation with the Navigation Component.
Learn Kotlin with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 36
- Lessons
- 152
Frequently Asked Questions
Is the “Adapters & DiffUtil” lesson free?
Yes — any 3 lessons of the Android Academy learning path, including “Adapters & DiffUtil”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Android Academy course includes 6 lessons in total.
What will I learn in “Adapters & DiffUtil”?
Build efficient, animated list adapters with ListAdapter and DiffUtil.ItemCallback. Use submitList() for smooth, partial updates. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Android Academy?
No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 6 of 6.
How long does the “Adapters & DiffUtil” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Android Academy lesson?
Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Kotlin Collections
- RecyclerView Basics
- RecyclerView Click Events
- SharedPreferences
- DataStore Preferences
- Adapters & DiffUtil