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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Speed up Your Android RecyclerView Using DiffUtil
25 mins
- Getting Started
- Understanding the Project Structure
- Getting to Know DiffUtil
- Understanding the DiffUtil Algorithm
- Creating Your RecyclerView With ListAdapter
- Adding DiffUtil to ListAdapter
- Updating ListAdapter’s Data References
- Accessing ListAdapter’s Data
- Comparing References and Content
- Using DiffUtil on a Background Thread
- Using DiffUtil in Any RecyclerView Adapter
- Using Payloads
- Animating Your RecyclerView With DiffUtil
- DiffUtil in Jetpack Compose
- Where to Go From Here?
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:
-
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 overrideareItemsTheSame
andareContentsTheSame
to provide this information. - An
Item
consists of anid
, itsvalue
,timeStamp
, and information stating if it’sdone
(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 sameid
. - 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. :]
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. :]
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
.
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. :]
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:
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.
[/spoiler]