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?
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.
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.
[/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:
-
onBindViewHolder
has to be overridden. This is why you need to add a second one that contains thepayload
as an argument. The first method calls the second one with the payload argument set as anemptyList()
. - Call this method when there’s no change from 1, or when there’s a difference on the
DiffCallback
, calculated ongetChangePayload
. - If the
payload
list is empty, then the object is new and the view needs to be drawn. - 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! :]
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.
- Updating the number of elements or their order
- Modifying an existing item
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.
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.
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.