UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Android & Kotlin Tutorials

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.

5/5 3 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

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.
Note: This tutorial assumes you’re familiar with Android Studio and the basics of Android development. If you’re new to either, read through Beginning Android Development and Kotlin for Android: An Introduction before continuing.

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.

Bring Cookies feature set: add a new item, mark items as done and randomize list

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.

The directory structure of the project.

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 of id, its value, the timeStamp 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.
Note: In this tutorial, you’ll use RecyclerView to display your grocery list. To learn more advanced content about how to use this view, check out Android RecyclerView Tutorial with Kotlin or Intermediate RecyclerView Tutorial with Kotlin.

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.

Distributed the lists: ANDROID and DIORDNA into a matrix to apply the Myers algorithm.

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.

The Myers algorithm applied to the lists: ANDROID and DIORDNA.

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.

Note: You can find more information about the Myers algorithm in this blog post from James Coglan.

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 from androidx.recyclerview.widget.
  • ListAdapter from android.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:

  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

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.

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:

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]

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:

  1. Updating the number of elements or their order
  2. 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.

  3. Modifying an existing item
  4. 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.

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.

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.

Note: You can find more information about Jetpack Compose in the Jetpack Compose by Tutorials book or from this video course.

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.

Average Rating

5/5

Add a rating for this content

3 ratings

More like this

Contributors

Comments