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 2 of 4 of this article. Click here to view the first page.

Adding DiffUtil to ListAdapter

Now that you’re extending ListAdapter, it displays an error. This is because it requires a class that implements DiffUtil.ItemCallback.

Add the following code above the ItemViewHolder inner class and import androidx.recyclerview.widget.DiffUtil:

//1
private class DiffCallback : DiffUtil.ItemCallback<Item>() {
 
  //2
  override fun areItemsTheSame(oldItem: Item, newItem: Item) =
    oldItem.id == newItem.id

  //3
  override fun areContentsTheSame(oldItem: Item, newItem: Item) =
    oldItem == newItem
}

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

  1. DiffUtil.ItemCallback is the native class responsible for calculating the difference between the two lists. Since the OS doesn’t know which fields to edit, it’s the app’s responsibility to override areItemsTheSame and areContentsTheSame to provide this information.
  2. An Item consists of an id, its value, timeStamp, and information stating if it’s done (checked) or not. id is unique and unchangeable, but you can edit all the other fields. So, you can consider two items, from different lists, to be the same if they share the same id.
  3. To avoid redesigning the entire list when there’s a change, only the items that have different values between both lists will be updated.

With the callback created, add it to the class declaration:

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

DiffCallback is now the argument of ListAdapter, and it’s responsible for comparing the existing list to the new one to identify the changed cells that need to be drawn.

Updating ListAdapter’s Data References

ListAdapter holds the list data in an inner field called currentList. To update the data, call submitList.

It’s no longer necessary to handle currently existing logic on MainAdapter, so remove var items: List = emptyList().

This will trigger a couple of errors in the project, so you’ll need to update all the references to this list.

Go to onBindViewHolder and replace:

 
val item = getItem(pos)

with:

 
val item = currentList[pos]

getItem(pos) returns the object at the specified position and its equivalent to call currentList[pos].

The next method to modify is getItemCount. Replace:

 
return items.size

with:

 
return currentList.size

currentList contains all the items.

Delete setListItems, since this logic is now handled by DiffUtil.

Finally, there are three more references to items. Inside bind, update the two occurrences of items.toMutableList() to currentList.toMutableList().

This corresponds to the user action that’s handled on MainFragment.

The last use of items is inside getSelectionKey, at the bottom of the adapter class.

Replace items by calling getItem, as shown below:

 
override fun getSelectionKey(): Long = getItem(adapterPosition).timeStamp

That’s it! You updated MainAdapter.kt and it’s ready to use DiffUtil.

Build and run.

You’ll see there are a couple of errors on MainFragment.kt. The app is trying to access items on MainAdapter.kt, which no longer exists. Instead, it should be using ListAdapter.currentList, which you’ll learn next.

Accessing ListAdapter’s Data

Open MainFragment.kt and go to onOptionsItemSelected.

The shuffle action shuffles the groceries. By changing the order, you can see the integration of DiffUtil with ItemAnimator, which results in a smooth animation that reorders all the elements.

Additionally, it challenges the user to keep track of everything that’s necessary to buy. :]

Go to onOptionsItemSelected and replace:

 
 val items = adapter.items.toMutableList()

with:

 
 val items = adapter.currentList.toMutableList()

currentList gets all the items in the adapter.

Also, replace:

 
 adapter.setListItems(items)

with:

 
 adapter.submitList(items)

submitList sets a shuffled version of the list.

One thing to note: You can’t shuffle Cookies. It always stays at the top of the list. :]

Go to setupUiComponents and update the first line inside the setOnClickListener of ivAddToCart to:

val list = mainAdapter.currentList.toMutableList()

Clicking this view creates a new item and adds it to the list.

At the end of this method, there’s a call to setListItems, which no longer exists. Replace it with:

mainAdapter.submitList(getGroceriesList(requireContext()))

This accesses ListAdapter directly.

The next update is similar. Go to updateAndSave and update the call for setListItems to submitList instead:

(binding.rvGroceries.adapter as MainAdapter).submitList(list)

Every time you add or remove a new item, it submits a new list to MainAdapter.kt, which is saved in the app’s Shared Preferences.

Finally, head to onActionItemClicked and modify both the references of items to currentList so that the modification looks like below:

var selected = mainAdapter.currentList.filter {
  tracker.selection.contains(it.timeStamp)
}

val groceries = mainAdapter.currentList.toMutableList()

This allows you to directly access all the items on currentList.

You’re almost there except an error in one class: ItemsKeyProvider, which you use for long-press actions.

Open this file and change both the references of items to currentList:

override fun getKey(position: Int): Long =
  adapter.currentList[position].timeStamp

override fun getPosition(key: Long): Int =
  adapter.currentList.indexOfFirst { it.timeStamp == key }

Build and run. Then add some groceries. :]

Adding more items to the groceries list

SelectionTracker, which is declared and initialized in MainFragment, allows the selection library to track the selections of the user to check if a specific item is selected or not. For more information on the selection library, please refer to the Android documentation.

2. You can delete the selected items using Delete, which was created using ActionMode. For more information on the Action Mode, please refer to the Android documentation. Again, it’s not possible to delete Cookies from the list. :]

Note: 1. The sample app uses the RecyclerView Selection library, which can select one or more items from the list. Item‘s timeStamp is the selection key type. ItemKeyProvider is the KeyProvider. ItemDetailsLookup is the class that provides the selection library information about the items associated with the user’s selection based on a MotionEvent, with the help of the the getItem created in ViewHolder.

Comparing References and Content

setupUiComponents on MainFragment.kt defines the update of an item:

element = if (index == 0) {
  Snackbar.make(binding.clContainer, R.string.item_more_cookies, Snackbar.LENGTH_SHORT).show()
  element.copy(done = false)

} else {
  element.copy(done = isChecked)
}

When the user marks an item as done, it creates a new copy of this object with this field changed to isChecked, which corresponds to true. If they deselect it, it’s false.

As an exercise, instead of creating a new object, update its value directly and see how the app behaves.

[spoiler title=”Solution”]

if (index == 0) {
  Snackbar.make(binding.clContainer, R.string.item_more_cookies, Snackbar.LENGTH_SHORT).show()
  element.done = false

} else {
  element.done = isChecked
}

Build and run the app and mark an element as done. You should see something like this:

UI doesn't refresh when an item is marked as done

This behavior is because the list that you’re accessing is the same as the one on ListAdapter. You’re changing the item itself, so when you call submitList, both lists will be the same and nothing happens. The oldItem from DiffCallback is going to be the same as newItem.

Revert this change.

Build and run. Mark one of the items as done to guarantee that everything is working as expected.

Marking items as done and not done

[/spoiler]