Android Academy · Lesson

Adapters & DiffUtil

Build efficient, animated list adapters with ListAdapter and DiffUtil.ItemCallback. Use submitList() for smooth, partial updates.

Lesson 6 of 613 steps

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 RecycledViewPool to 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; call submitList()
  • Always submit a new list reference, never mutate the current list
  • getChangePayload() for partial rebinds
  • ConcatAdapter to compose multiple adapters

Next: multi-screen navigation with the Navigation Component.

Free to start

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

  1. Kotlin Collections
  2. RecyclerView Basics
  3. RecyclerView Click Events
  4. SharedPreferences
  5. DataStore Preferences
  6. Adapters & DiffUtil
← Back to Android Academy