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 — lesson 6 of 6. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 6 lessons in the course, and your progress syncs across the web and the CoddyKit app.
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.
Frequently asked questions
Is the “Adapters & DiffUtil” lesson free?
Yes — the full text of “Adapters & DiffUtil” is free to read here on the web, and the Android Academy course includes 6 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.
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; this is — lesson 6 of 6, so you can start here or from the beginning and move at your own pace.
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.