Speed up Your Android RecyclerView Using DiffUtil

Learn how to update the Android RecyclerView using DiffUtil to improve the performance. Also learn how it adds Animation to RecyclerView. By Carlos Mota.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Using DiffUtil on a Background Thread

The difference between DiffUtil and AsyncListDiffer is that the latter runs on a background thread. This makes it ideal for long-running operations or using it along with LiveData.

To implement AsyncListDiffer with ListAdapter, open MainAdapter.kt and change the class declaration to:

 
class MainAdapter(val action: (items: MutableList<Item>, changed: Item, checked: Boolean) -> Unit) :
    ListAdapter<Item, MainAdapter.ItemViewHolder>(AsyncDifferConfig.Builder<Item>(DiffCallback()).build())

Import androidx.recyclerview.widget.AsyncDifferConfig.

Instead of sending DiffCallback directly, AsyncDifferConfig.Builder creates an asynchronous object, which uses the DiffUtil created before.

Build and run. You’ve been adding a lot of groceries, so delete a couple items to confirm everything is working as expected.

Remove all checked groceries

Using DiffUtil in Any RecyclerView Adapter

Although ListAdapter is the recommended RecyclerView.Adapter to use with DiffUtil, it’s possible to use with any adapter. The difference is that it’s necessary to declare a variable that holds the DiffCallback and the corresponding currentList and submitList to access and edit the list that doesn’t exist in the other adapters.

As an exercise, open MainAdapter.kt, change the class declaration to extend RecyclerView.Adapter and implement the AsyncListDiffer.

[spoiler title=”Solution”]

First, change ListAdapter to RecyclerView.Adapter:

class MainAdapter(val action: (items: MutableList<Item>, changed: Item, checked: Boolean) -> Unit) :
    RecyclerView.Adapter<MainAdapter.ItemViewHolder>()

Import androidx.recyclerview.widget.AsyncListDiffer.

With this change, DiffCallback is no longer set, and since currentList is a property of ListAdapter, it’s no longer accessible.

Now, declare AsyncListDiffer along with the DiffCallback you created before:

 
private val differ: AsyncListDiffer<Item> = AsyncListDiffer(this, DiffCallback())

This field contains the adapter list. To access it, on onBindViewHolder, call:

differ.currentList[pos]

On getItem, on the bottom of the adapter class, change getItem(adapterPosition).timeStamp to:

differ.currentList[adapterPosition].timeStamp

When you build the project, it displays all the references that need an update. To more easily convert the existing project to AsyncListDiffer, create the following methods:

 
fun submitList(list: List<Item>) {
  differ.submitList(list)
}

fun currentList(): List<Item> {
  return differ.currentList
}

These method’s names are similar to the ones you called previously, so changes will be minimal.

Now, update the calls for currentList to:

currentList()

This change is required, as it now refers to the method instead of the field.

Build and run. Add and remove a couple of items to and from the list.

Add groceries to the list and mark them as checked using DiffUtil

[/spoiler]

Using Payloads

You can use payloads when list cells contain several views and when an update in one element doesn’t require a redesign of the entire view. They’re particularly useful when you want to avoid fetching the same image or performing heavy calculations.

First, open MainAdapter.kt. Before the class declaration, add:

 
private const val ARG_DONE = "arg.done"

You can use this to identify if done on Item changed and the list updates.

Go to DiffCallback and override getChangePayload:

 
override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
  if (oldItem.id == newItem.id) {
    return if (oldItem.done == newItem.done) {
      super.getChangePayload(oldItem, newItem)
    } else {
      val diff = Bundle()
      diff.putBoolean(ARG_DONE, newItem.done)
      diff
    }
  }

  return super.getChangePayload(oldItem, newItem)
}

Import android.os.Bundle.

Note here that getChangePayload is a non-abstract method. This method is called when areItemsTheSame returns true and areContentsTheSame returns false. This indicates that some of the Item fields changed. Nevertheless, it’s a good practice to compare the id of items to guarantee that it’s the same one.

In this case, the field modified is done, so in case its state is different, a Bundle returns with the information that changed.

Go to ItemViewHolder and add update:

 
fun update(bundle: Bundle) {
  if (bundle.containsKey(ARG_DONE)) {
    val checked = bundle.getBoolean(ARG_DONE)
    itemBinding.cbItem.isChecked = checked
    setItemTextStyle(checked)
  }
}

Only the views that use done update. This avoids wasting resources on updating fields that didn’t change.

In this example, MainAdapter extends ListAdapter. If you’re using RecyclerView.Adapter, make the appropriate changes.

Head to onBindViewHolder, add onBindViewHolder to receive the payload as an argument and update the existing one:

 
//1
override fun onBindViewHolder(holder: ItemViewHolder, pos: Int) {
  onBindViewHolder(holder, pos, emptyList())
}
 
//2
override fun onBindViewHolder(viewHolder: ItemViewHolder, pos: Int, payload: List<Any>) {
  val item = getItem(pos)
 
  if (payload.isEmpty() || payload[0] !is Bundle) {
    //3
    viewHolder.bind(item)
   } else {
    //4
    val bundle = payload[0] as Bundle
    viewHolder.update(bundle)
  }
}

Here’s a step-by-step breakdown of this logic:

  1. onBindViewHolder has to be overridden. This is why you need to add a second one that contains the payload as an argument. The first method calls the second one with the payload argument set as an emptyList().
  2. Call this method when there’s no change from 1, or when there’s a difference on the DiffCallback, calculated on getChangePayload.
  3. If the payload list is empty, then the object is new and the view needs to be drawn.
  4. In case the payload contains some data, it means an update on that object. So, you can reuse some of its views.

Build and run. Add the payload, go to the groceries and check some items off.

And here’s a cookie recipe! :]

Cookies recipe as groceries list using DiffUtil

Animating Your RecyclerView With DiffUtil

Another advantage of DiffUtil is that every update on your list results in a smooth, beautiful animation. The content doesn’t just switch — instead, it smoothly adapts to the new data.

You can have two different types of animations that are already built into this implementation of DiffUtil inside a RecyclerView:

When you add a new item or randomly change its order, you can see that the elements don’t just pop up on the screen. Instead, they appear through an animated transition.

This only updates the elements that are visible and have changed. There’s a smooth transition from one state to the next that notifies the user about a changed object.

  1. Updating the number of elements or their order
  2. Modifying an existing item

Changing the groceries order with animations using DiffUtil

This is possible due to the native integration of DiffUtil along ItemAnimator from RecyclerView. Changing ItemAnimator will automatically reflect on any update made to the list.

Alternatively, setting binding.rvGroceries.itemAnimator = null will remove all the animations.

The default value of itemAnimator is DefaultItemAnimator. This already has the animations for the add, remove and move elements defined.

Changing the groceries order with no animations

Note: Want to know more about RecyclerView list animations? You can define them by changing the value of ItemAnimator. To learn more, take a look at the following section of the Beginning RecyclerView course: Part 3: Decorating and Animating.