Skip to main content
PCSalt
YouTube GitHub
Back to Android
Android · 2 min read

Modern RecyclerView — ListAdapter, DiffUtil & ViewBinding in Kotlin

Build efficient RecyclerView lists with ListAdapter, DiffUtil.ItemCallback, and ViewBinding in Kotlin — with click handling, ViewModel integration, and common pitfalls.


If you’ve been calling notifyDataSetChanged() on your RecyclerView adapter, stop. It tells RecyclerView that every item has changed, which kills item animations, forces a full rebind of every visible item, and makes your list feel janky. There’s a better way.

ListAdapter + DiffUtil calculate the exact difference between your old and new lists on a background thread, then apply minimal updates — inserts, removes, moves, and changes. Your list gets smooth animations for free.

DiffUtil.ItemCallback

DiffUtil needs two pieces of information: are these the same item (identity), and if so, has the content changed?

import androidx.recyclerview.widget.DiffUtil

data class Article(
    val id: Long,
    val title: String,
    val author: String,
    val publishedAt: String
)

class ArticleDiffCallback : DiffUtil.ItemCallback<Article>() {

    override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
        return oldItem == newItem
    }
}

areItemsTheSame checks identity — usually a database ID or unique key. areContentsTheSame checks whether the item needs rebinding. Since Article is a data class, the generated equals() compares all properties.

Building the ListAdapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.databinding.ItemArticleBinding

class ArticleAdapter(
    private val onItemClick: (Article) -> Unit
) : ListAdapter<Article, ArticleAdapter.ArticleViewHolder>(ArticleDiffCallback()) {

    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))
    }

    inner class ArticleViewHolder(
        private val binding: ItemArticleBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        init {
            binding.root.setOnClickListener {
                val position = bindingAdapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    onItemClick(getItem(position))
                }
            }
        }

        fun bind(article: Article) {
            binding.textTitle.text = article.title
            binding.textAuthor.text = article.author
            binding.textDate.text = article.publishedAt
        }
    }
}

Key points:

  • The click handler uses a lambda passed to the adapter — no interface needed
  • bindingAdapterPosition is checked against NO_POSITION to avoid crashes during animations
  • getItem(position) comes from ListAdapter — no need to maintain your own list

Wiring it up with ViewModel

import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.databinding.FragmentArticlesBinding
import kotlinx.coroutines.launch

class ArticlesFragment : Fragment(R.layout.fragment_articles) {

    private val viewModel: ArticlesViewModel by viewModels()
    private var _binding: FragmentArticlesBinding? = null
    private val binding get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding = FragmentArticlesBinding.bind(view)

        val adapter = ArticleAdapter { article ->
            // handle click
        }

        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            this.adapter = adapter
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.articles.collect { articles ->
                    adapter.submitList(articles)
                    binding.emptyState.isVisible = articles.isEmpty()
                }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

submitList() triggers DiffUtil on a background thread. When diffing completes, RecyclerView applies the minimal set of changes with animations.

Handling empty state

The pattern above already handles it — check articles.isEmpty() after each submitList() and toggle an empty state view. Keep it simple: a TextView with isVisible is enough.

Common mistakes

Submitting the same list reference

// WRONG — DiffUtil sees same reference, skips diffing
val list = mutableListOf<Article>()
list.add(newArticle)
adapter.submitList(list) // nothing happens

// RIGHT — always submit a new list
adapter.submitList(list.toList())

ListAdapter short-circuits if the new list is the same object reference. Always create a new list instance.

Bad areContentsTheSame

If areContentsTheSame returns true when content has actually changed, the item won’t rebind. Using data class equality avoids this — but if your model has fields you display that aren’t in the constructor, equality won’t catch them.

Setting click listeners in onBindViewHolder

// WRONG — creates a new lambda on every bind
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
    holder.binding.root.setOnClickListener { onItemClick(getItem(position)) }
    // ...
}

// RIGHT — set it once in the ViewHolder init block (shown above)

Setting listeners in init means one allocation per ViewHolder instead of one per bind call.

Moving forward

ListAdapter + DiffUtil is the right approach for RecyclerView in 2026. It replaces the manual notifyItem* calls from the old RecyclerView approach with automatic, efficient diffing.

If you’re starting a new screen, consider whether Jetpack Compose’s LazyColumn is a better fit — it handles diffing internally and eliminates the adapter/ViewHolder boilerplate entirely. For existing View-based code, ListAdapter is the modern standard.