0Pricing
Android Academy · Lesson

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 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.

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.

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