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?
Android RecyclerViews displaying some sort of list are part of almost every Android application in the real world. Lists hold a lot of information, so it’s important to provide a smooth experience both when a user is scrolling through a list and when its content is updated. DiffUtil is a utility class developed to help with this, and Android RecyclerView using DiffUtil provides this feature.
In this tutorial, you’ll build a grocery list app. It uses DiffUtil to avoid redrawing all the cells in a list when only a subset of its data changes. During this process, you’ll learn:
- How to implement DiffUtil with ListAdapter.
- To convert your ListAdapter into any class that extends RecyclerView.Adapter.
- To use payloads.
- How DiffUtil adds Animation to RecyclerView.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.
You’ll find two projects inside the ZIP file. Starter has the skeleton of the app you’ll build, and Final gives you something to compare your code to when you’re done.
In the image above, you see a grocery list. You can probably relate to returning home from the store, only to realize you forgot to buy everything you needed. The best approach is to write a shopping list in advance, but sometimes those get left at home. But you most likely always have your phone with you.
So, you’re going to create BringCookies, a grocery list app that helps you keep track of everything you need to buy on your next trip to the store. And it has a bonus feature! The item Cookies is always on the list, because there’s always room for more cookies. :]
But first, you’ll take some time to understand the project structure.
Understanding the Project Structure
Open the Starter project in Android Studio and wait for it to synchronize.
You’ll see a set of subfolders and other important files:
-
adapters: Contains the
RecyclerView
adapter and a set the utilities used to select one or more items from the list. -
model: Contains the
Item
data object to represent an item.Item
consists ofid
, itsvalue
, thetimeStamp
of when it was created and a Boolean flag —done
— that indicates if item was checked or not. - MainActivity: The app’s single activity.
- MainFragment: Responsible for displaying the grocery list to the user and providing a set of mechanisms with which the user can interact.
- Utils.kt: This file contains a set of utility methods you’ll use throughout the project. Namely, you’ll use these to save and load your grocery list into and from shared preferences and format a note timestamp to an easily readable date.
Moving on, it’s time to learn more about DiffUtil.
Getting to Know DiffUtil
The DiffUtil utility class exists to improve RecyclerView’s performance when handling list updates. Even if associated with this UI component, you can use it in any part of your app to compare two lists of the same type. In the case of this app, you’ll want to check the differences between two lists of type Item.
For the algorithm used by DiffUtil to work, the lists must be immutable. Otherwise, if their content changes, the result might be different than expected. Hence, to update an item in the list, create and set a copy of that element.
Understanding the DiffUtil Algorithm
To see the difference between two lists — in the case of RecyclerView, the one that you’re already showing and the one that you want to show (when any of items in the list changes) — DiffUtil uses the Eugene W. Myers difference algorithm. This calculates the difference between both sets of elements.
Myers algorithm does not handle elements that are moved so DiffUtil runs a second pass on the result that detects which elements moved.
This image has two lists of words spread through a grid: ANDROID horizontally and DIORDNA vertically.
The algorithm calculates the shortest path from one list to the other. Diagonals are free steps, and for this reason, they don’t count for the number of iterations required.
Starting on (0, 0), which corresponds to the character ‘A’, the Myers algorithm goes through each point in the matrix, looking for the shortest path to transform one list into the other.
From this initial point, it can go down to (0, 1), or right to (1, 0):
- From (0, 1), it can go to (0, 2) or (1, 1).
- From (1, 0), can go to (1, 2) or (2, 0).
At this last coordinate, (2, 0), there’s a diagonal insight, which means it’s possible to reach directly to (3, 1), thereby saving a couple steps.
Following this path, you can go from (0, 0) → (0, 1) → (1, 0) → (3, 1), skipping (2, 0) and (3, 0).
The algorithm will analyze all the possible paths, selecting the shortest one to convert one list into the other. In this example, it takes eight iterations.
Creating Your RecyclerView With ListAdapter
ListAdapter
is an extension of RecyclerView.Adapter
that requires initializing DiffUtil.ItemCallback
or AsyncDifferConfig
. It provides a set of methods that allows you to easily access the adapter’s data, but you can use any available adapter.
So first, open MainAdapter.kt.
This class extends the default implementation of RecyclerView.Adapter
. Change it to use ListAdapter
:
class MainAdapter(val action: (items: MutableList<Item>, changed: Item, checked: Boolean) -> Unit) : ListAdapter<Item, MainAdapter.ItemViewHolder>()
Android Studio prompts two imports:
-
ListAdapter
fromandroidx.recyclerview.widget
. -
ListAdapter
fromandroid.widget
.
Import the first one, androidx.recyclerview.widget
, to successfully access the adapter.
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. :]
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]
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
:
- 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.
DiffUtil in Jetpack Compose
Jetpack Compose is a new set of libraries that allow you to develop your UI declaratively. You no longer need to rely on XML and findViewById to set and update a view. You can do everything programmatically using the concepts of state and recomposition.
With Compose, you can create a list using LazyColumn
:
@Composable
fun Groceries() {
val groceries = remember { mutableStateOf(getGroceriesList(context)) }
LazyColumn {
items(groceries.value) {
AddGrocery(it)
}
}
}
@Composable
fun AddGrocery(item: Item) {
Column {
Text(
text = item.content
)
}
}
Alternatively, if you want to create a horizontal list, you can use LazyRow
.
Jetpack Compose works by recomposition. The redesign happens only for functions with changed content. This has direct improvements on performance since it only redraws views that changed. Due to this, there’s currently no implementation on LazyColumn
and LazyRow
that works with DiffUtil
directly.
Another advantage of DiffUtil
is that every time there’s an update on the list, it uses ItemAnimator
to create smooth animations, without the need to write additional code.
Although not directly supported out of the box in Compose, the same is possible by using AnimatedVisibility
and animating all the views.
Curious about how to implement this? Read the book chapter: Animating Properties Using Compose to learn more.
Where to Go From Here?
Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
Congratulations! You learned how to improve your list’s performance and how to create a smooth update animation when updating its content.
There are a couple of scenarios in which lists can have bad performance. You’ll find a set of examples of this in the Slow Rendering section of the Android documentation. Another solution to overcome this issue is the Paging library, which only loads the information that’s necessary to show on the screen.
In this tutorial, you read a bit about Jetpack Compose. Curious about how to create an app using these new libraries? Follow the Getting Started tutorial to learn more.
If you have any questions or comments, please join the discussion below.