Search

Efficient lists with DiffUtil and ListAdapter

Anthony Kiniyalocts

5 min read

Jun 3, 2021

Efficient lists with DiffUtil and ListAdapter

Writing code to display lists of data is an everyday task as a software engineer, regardless of platform. Operations to modify lists can vary in complexity, and force developers down paths that sacrifice user experience or performance. This article will show you how to leverage DiffUtil and ListAdapter to avoid those pitfalls, and provide a more efficient user experience, while reducing boilerplate code.

On Android, RecyclerView is the go-to API for efficiently displaying lists of data. Our data is often changing, so out of the box, RecyclerView.Adapter comes with many low-level granular APIs to update our user interface as the backing data changes, such as:

notifyItemChanged(position: Int)
notifyItemRemoved(position: Int)
notifyItemRangeChanged(positionStart: Int, itemCount: Int)

and many more

In order to explore these APIs, I’ve created a small sample app listing the known planets of our solar system shown in descending order by their distance from the sun. It seems Pluto has snuck its’ way into our list, so let’s remove it with notifyItemRemoved():

adapter.items.remove(pluto)
adapter.notifyItemRemoved(plutoIndex)

Assuming you have access to the position of the item to be removed, we can make the appropriate method calls to remove the item from the list and provide smooth animations for our user to visualize the change.

These types of interactions are manageable for small changes but don’t scale well. For a more complex example, let’s have some of our planets change position. (Please ignore the real-world implications of this 🙈) Jupiter moves in front of Saturn, Mars and Earth swap, and Venus is flung out beyond Neptune. Resulting in an order like this:

[ Venus, Neptune, Uranus, Jupiter, Saturn, Earth, Mars, Mercury ]

How would we go about showing these changes to the user with the RecyclerView.Adapter APIs we have available? notifyItemMoved(fromPosition: Int, toPosition: Int) comes to mind… But that would involve keeping track of each items’ individual movement. These low-level APIs can be tedious to implement for complex use-cases, and are not always kept in mind when architecting your application. This tends to lead developers to use the “catch-all” notifyDataSetChanged() method when updating their adapters. From the official documentation, we learn this is not ideal for the best user experience:

This event does not specify what about the data set has changed, forcing any observers to assume that all existing items and structure may no longer be valid. LayoutManagers will be forced to fully rebind and relayout all visible views.

And even goes on to say:

If you are writing an adapter it will always be more efficient to use the more specific change events if you can. Rely on notifyDataSetChanged() as a last resort.

Here is an example of notifyDataSetChanged() displaying our new list of rearranged planets:

adapter.items = rearrangedPlanets

adapter.notifyDataSetChanged()

Our data is displayed accurately, and the steps to make that change were much easier, but it resulted in a jarring user experience along with inefficient use of RecyclerView APIs.

Fortunately, there have been many additions that augment the ease-of-use of RecyclerView and RecyclerView.Adapter.

One in particular that has gained some much-needed attention since release is DiffUtil. From the official documentation:

DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one.

Seems simple enough. I have my oldList, and my newList. What are the minimum amount of steps needed to migrate oldList => newList and display those steps to my user? –A perfect use-case to display the milky-way’s newly arranged solar system! 🪐

Create a new class, extending DiffUtil.Callback. We supply this class with our new and old list and it calculates a DiffResult.

areItemsTheSame() determines if oldItem and newItem are the same item (usually represented by comparing a unique id). areContentsTheSame() determines if the values of newItem and oldItem have changed. A result is calculated determining the item’s new position (if it was moved), and it’s new contents (if it has changed).

class PlanetDiffUtilCallback(private val oldList: List<Planet>, private val newList: List<Planet>): DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == newList[newItemPosition]
    }
}

Add a method to our PlanetAdapter (a subclass of RecyclerView.Adapter):

fun update(newList: List<Planet>){
    val callback = PlanetDiffUtilCallback(items, newList)
    val result = DiffUtil.calculateDiff(callback)
    currentItems = newList // don't forget to update the backing list
    result.dispatchUpdatesTo(this)
}

From our Activity or Fragment:

planetBasicAdapter.update(rearrangedPlanets)

How wonderful! Each modified planet in our RecyclerView is updated with its new location, and the user is notified of the granular changes in the list with smooth animations 😎.

As perfect as DiffUtil seems, it does come with some limitations. From the documentation :

The actual runtime of the algorithm significantly depends on the number of changes in the list and the cost of your comparison methods… Due to implementation constraints, the max size of the list can be 2^26.

So DiffUtil could run into issues on very large lists with heavy modifications. It’s also worth noting, that in our current implementation, the “diffing” calculation is being done on the main thread. This could lead to unwanted UI “jank”. Fortunately, DiffUtil offers a way to move its calculation to a background thread.

To send DiffUtil calculations to the background, Android offers us AsyncListDiffer. For simplicity, the ListAdapter wrapper class can be used instead of AsyncListDiffer directly. Below is a modified version of our above example, using ListAdapter:

class PlanetListAdapter: ListAdapter<Planet, PlanetViewHolder>(PlanetDiff) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlanetViewHolder {
        return PlanetViewHolder(ItemPlanetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: PlanetViewHolder, position: Int) {
        holder.bind(getItem(position))
    }
}

PlanetDiff object:

object PlanetDiff: DiffUtil.ItemCallback<Planet>(){
    override fun areContentsTheSame(oldItem: Planet, newItem: Planet): Boolean {
        return oldItem == newItem
    }

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

ListAdapter‘s implementation requires us to override onCreateViewHolder() and onBindViewHolder(), 1 less method than a normal RecyclerView.Adapter. For most use-cases, ListAdapter should be our go-to RecyclerView.Adapter class because of its reduced boilerplate and added functionality.

From our Activity or Fragment:

planetAdapter.submitList(rearrangedPlanets)

That’s it! Now the application is performing its diff calculation on a background thread, saving our user from any potential lag. We also wrote less code and reduced boilerplate using ListAdapter.

This simple example shows how ListAdapter and DiffUtil can be used to avoid hardship without sacrificing performance while displaying and modifying lists of data. APIs like this have been a great addition to the Android platform, so much so that other platforms have followed suit, implementing their own versions of DiffUtil, all based on the Myers difference algorithm.

Juan Pablo Claude

Reviewer Big Nerd Ranch

During his tenure at BNR, Juan Pablo has taught bootcamps on macOS development, iOS development, Python, and Django. He has also participated in consulting projects in those areas. Juan Pablo is currently a Director of Technology focusing mainly on managing engineers and his interests include Machine Learning and Data Science.

Speak with a nerd

Schedule a call today! Our team of nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Stay in Touch WITH Big Nerd Ranch News