Android Kotlin Fundamentals: 07.2 DiffUtil and data binding with RecyclerView

1. Welcome

Introduction

In the previous codelab, you updated the TrackMySleepQuality app to display data about sleep quality in a RecyclerView. The techniques you learned when you built your first RecyclerView are sufficient for most RecyclerViews that display simple lists where the source data doesn't change much. However, there are a number of techniques that make RecyclerView more efficient for large lists. These approaches make your code easier to maintain and extend for complex lists and grids.

In this codelab, you build on the sleep-tracker app from the previous codelab. You learn a more effective way to update the list of sleep data. You also learn how to use data binding with RecyclerView. If you don't have the app from the previous codelab, you can download starter code for this codelab.

What you should already know

  • Building a basic user interface using an activity, fragments, and views.
  • Navigating between fragments, and using safeArgs to pass data between fragments.
  • View models, view model factories, transformations, and LiveData and their observers.
  • How to create a Room database, create a DAO, and define entities.
  • How to use coroutines for database and other long-running tasks.
  • How to implement a basic RecyclerView with an Adapter, ViewHolder, and item layout.

What you'll learn

  • How to use DiffUtil, a utility that calculates the difference between two lists to efficiently update a list displayed by RecyclerView.
  • How to use data binding with RecyclerView.
  • How to use binding adapters to transform data.

What you'll do

  • Build on the TrackMySleepQuality app from the previous codelab in this series.
  • Update the SleepNightAdapter to efficiently update the list using DiffUtil.
  • Implement data binding for the RecyclerView, using binding adapters to transform the data.

2. App overview

The sleep-tracker app has two screens, represented by fragments, as shown in the figure below.

The first screen, shown on the left, has buttons for starting and stopping sleep-quality tracking. This screen shows some of the user's sleep data. The Clear button permanently deletes all the data that the app has collected for the user. The second screen, shown on the right, is for selecting a sleep-quality rating.

This app is architected to use a UI controller, ViewModel and LiveData, and a Room database to persist sleep data.

f24b713c78982a7f.png

The sleep data is displayed in a RecyclerView. In this codelab, you build the DiffUtil and data-binding portion for the RecyclerView. After this codelab, your app will look exactly the same, but it will be more efficient and easier to scale and maintain.

3. Task: Get started and review what you have so far

You can continue using the SleepTracker app from the previous codelab, or you can download the RecyclerViewDiffUtilDataBinding-Starter app from GitHub.

  1. If needed, download the RecyclerViewDiffUtilDataBinding-Starter app from GitHub and open the project in Android Studio.
  2. Run the app.
  3. Open the SleepNightAdapter.kt file.
  4. Inspect the code to familiarize yourself with the structure of the app. Refer to the diagram below for a recap of using RecyclerView with the adapter pattern to display sleep data to the user.

9159809a2107f18e.png

  • From user input, the app creates a list of SleepNight objects. Each SleepNight object represents a single night of sleep, its duration, and quality.
  • The SleepNightAdapter adapts the list of SleepNight objects into something RecyclerView can use and display.
  • The SleepNightAdapter adapter produces ViewHolders that contain the views, data, and meta information for the recycler view to display the data.
  • RecyclerView uses the SleepNightAdapter to determine how many items there are to display (getItemCount()). RecyclerView uses onCreateViewHolder() and onBindViewHolder() to get view holders bound to data for displaying.

The notifyDataSetChanged() method is inefficient

To tell RecyclerView that an item in the list has changed and needs to be updated, the current code calls notifyDataSetChanged() in the SleepNightAdapter, as shown below.

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

However, notifyDataSetChanged() tells RecyclerView that the entire list is potentially invalid. As a result, RecyclerView rebinds and redraws every item in the list, including items that are not visible on screen. This is a lot of unnecessary work. For large or complex lists, this process could take long enough that the display flickers or stutters as the user scrolls through the list.

To fix this problem, you can tell RecyclerView exactly what has changed. RecyclerView can then update only the views that changed on screen.

RecyclerView has a rich API for updating a single element. You could use notifyItemChanged() to tell RecyclerView that an item has changed, and you could use similar functions for items that are added, removed, or moved. You could do it all manually, but that task would be non-trivial and might involve quite a bit of code.

Fortunately, there's a better way.

DiffUtil is efficient and does the hard work for you

RecyclerView has a class called DiffUtil which is for calculating the differences between two lists. DiffUtil takes an old list and a new list and figures out what's different. It finds items that were added, removed, or changed. Then it uses an algorithm called Eugene W. Myers's difference algorithm to figure out the minimum number of changes to make from the old list to produce the new list.

Once DiffUtil figures out what has changed, RecyclerView can use that information to update only the items that were changed, added, removed, or moved, which is much more efficient than redoing the entire list.

4. Task: Refresh list content with DiffUtil

In this task, you upgrade the SleepNightAdapter to use DiffUtil to optimize the RecyclerView for changes to the data.

Step 1: Implement SleepNightDiffCallback

In order to use the functionality of the DiffUtil class, extend DiffUtil.ItemCallback.

  1. Open SleepNightAdapter.kt.
  2. Below the full class definition for SleepNightAdapter, make a new top-level class called SleepNightDiffCallback that extends DiffUtil.ItemCallback. Pass SleepNight as a generic parameter.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. Put the cursor in the SleepNightDiffCallback class name.
  2. Press Alt+Enter (Option+Enter on Mac) and select Implement Members.
  3. In the dialog that opens, shift-left-click to select the areItemsTheSame() and areContentsTheSame() methods, then click OK.

This generates stubs inside SleepNightDiffCallback for the two methods, as shown below. DiffUtil uses these two methods to figure out how the list and items have changed.

    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. Inside areItemsTheSame(), replace the TODO with code that tests whether the two passed-in SleepNight items, oldItem and newItem, are the same. If the items have the same nightId, they are the same item, so return true. Otherwise, return false. DiffUtil uses this test to help discover if an item was added, removed, or moved.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. Inside areContentsTheSame(), check whether oldItem and newItem contain the same data; that is, whether they are equal. This equality check will check all the fields, because SleepNight is a data class. Data classes automatically define equals and a few other methods for you. If there are differences between oldItem and newItem, this code tells DiffUtil that the item has been updated.
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

5. Task: Use ListAdapter to manage your list

It's a common pattern to use a RecyclerView to display a list that changes. RecyclerView provides an adapter class, ListAdapter, that helps you build a RecyclerView adapter that's backed by a list.

ListAdapter keeps track of the list for you and notifies the adapter when the list is updated.

Step 1: Change your adapter to extend ListAdapter

  1. In the SleepNightAdapter.kt file, change the class signature of SleepNightAdapter to extend ListAdapter.
  2. If prompted, import androidx.recyclerview.widget.ListAdapter.
  3. Add SleepNight as the first argument to the ListAdapter, before SleepNightAdapter.ViewHolder.
  4. Add SleepNightDiffCallback() as a parameter to the constructor. The ListAdapter will use this to figure out what changed in the list. Your finished SleepNightAdapter class signature should look as shown below.
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. Inside the SleepNightAdapter class, delete the data field, including the setter. You don't need it anymore, because ListAdapter keeps track of the list for you.
  2. Delete the override of getItemCount(), because the ListAdapter implements this method for you.
  3. To get rid of the error in onBindViewHolder(), change the item variable. Instead of using data to get an item, call the getItem(position) method that the ListAdapter provides.
val item = getItem(position)

Step 2: Use submitList() to keep the list updated

Your code needs to tell the ListAdapter when a changed list is available. ListAdapter provides a method called submitList() to tell ListAdapter that a new version of the list is available. When this method is called, the ListAdapter diffs the new list against the old one and detects items that were added, removed, moved, or changed. Then the ListAdapter updates the items shown by RecyclerView.

  1. Open SleepTrackerFragment.kt.
  2. In onCreateView(), in the observer on sleepTrackerViewModel, find the error where the data variable that you've deleted is referenced.
  3. Replace adapter.data = it with a call to adapter.submitList(it). The updated code is shown below.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. Run your app. You may have to import findNavController. You may notice your app runs faster, maybe not noticeably if your list is small.

6. Task: Use DataBinding with RecyclerView

In this task, you use the same technique as in previous codelabs to set up data binding, and you eliminate calls to findViewById().

Step 1: Add data binding to the layout file

  1. Open the list_item_sleep_night.xml layout file in the Code tab.
  2. Put the cursor on the ConstraintLayout tag and press Alt+Enter (Option+Enter on a Mac). The intention menu (the "quick fix" menu) opens.
  3. Select Convert to data binding layout. This wraps the layout into <layout> and adds a <data> tag inside.
  4. Scroll back to the top, if necessary, and inside the <data> tag, declare a variable named sleep.
  5. Make its type the fully qualified name of SleepNight, com.example.android.trackmysleepquality.database.SleepNight. Your finished <data> tag should look as shown below.
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. To force the creation of the Binding object, select Build > Clean Project, then select Build > Rebuild Project. (If you still have problems, select File > Invalidate Caches / Restart.) The ListItemSleepNightBinding binding object, along with related code, is added to the project's generated files.

Step 2: Inflate the item layout using data binding

  1. Open SleepNightAdapter.kt.
  2. In the companion object , find the from(parent: ViewGroup) function.
  3. Delete the declaration of the view variable.

Code to delete:

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. Where the view variable was, define a new variable called binding that inflates the ListItemSleepNightBinding binding object, as shown below. Make the necessary import of the binding object.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. At the end of the function, instead of returning the view, return binding.
return ViewHolder(binding)
  1. To get rid of the error on binding, place your cursor on the word binding. Press Alt+Enter (Option+Enter on a Mac) to open the intention menu.
  2. Select Change parameter ‘itemView' type of primary constructor of class ‘ViewHolder' to ‘ListItemSleepNightBinding'. This updates the parameter type of the ViewHolder class.

63cccaef9963ad8.png

  1. Scroll up to the class definition of the ViewHolder to see the change in the signature. You see an error for itemView, because you changed itemView to binding in the from() method.

In the ViewHolder class definition, right-click on one of the occurrences of itemView and select Refactor > Rename. Change the name to binding.

  1. Prefix the constructor parameter binding with val to make it a property.
  2. In the call to the parent class, RecyclerView.ViewHolder, change the parameter from binding to binding.root. You need to pass a View, and binding.root is the root ConstraintLayout in your item layout.
  3. Your finished class declaration should look like the code below.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

You also see errors for the calls to findViewById(). You fix these errors in the next section.

Step 3: Replace findViewById()

You can now update the sleepLength, quality, and qualityImage properties to use the binding object instead of findViewById().

  1. Change the initializations of sleepLength, qualityString, and qualityImage to use the views of the binding object, as shown below. After this, your code should not show any more errors.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

With the binding object in place, you don't need to define the sleepLength, quality, and qualityImage properties at all anymore. DataBinding will cache the lookups, so there is no need to declare these properties.

  1. Right-click on the sleepLength, quality, and qualityImage property names. Select Refactor > Inline, or press Ctrl+Alt+N (Option+Command+N on a Mac) for each property. 1d9001b5d4de3e73.png
  2. Run your app. (You may need to Clean and Rebuild your project if it has errors.)

7. Task: Create binding adapters

In this task, you upgrade your app to use data binding with binding adapters to set the data in your views.

In a previous codelab, you used the Transformations class to take LiveData and generate formatted strings to display in text views. However, if you need to bind different types of data, or complex types, you can provide binding adapters to help data binding use those types. Binding adapters are adapters that take your data and adapt it into something that data binding can use to bind a view, like text or an image.

You are going to implement three binding adapters, one for the quality image, and one for each text field. In summary, to declare a binding adapter, you define a method that takes an item and a view, and annotate it with @BindingAdapter. In the body of the method, you implement the transformation. In Kotlin, you can write a binding adapter as an extension function on the view class that receives the data.

Step 1: Create binding adapters

Note that you will have to import a number of classes in this step.

  1. Open SleepNightAdapter.kt.
  2. Inside the ViewHolder class, find the bind() method and remind yourself what this method does. You will take the code that calculates the values for binding.sleepLength, binding.quality, and binding.qualityImage, and use it inside the adapter instead. (For now, leave the code as it is; you move it in a later step.)
  3. In the sleeptracker package, create a new file called BindingUtils.kt and open it.
  4. Delete everything in the BindingUtils class, because you are creating static functions next.
class BindingUtils {}
  1. Declare an extension function on TextView, called setSleepDurationFormatted, and pass in a SleepNight. This function will be your adapter for calculating and formatting the sleep duration.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. In the body of setSleepDurationFormatted, bind the data to the view as you did in ViewHolder.bind(). Call convertDurationToFormatted()and then set the text of the TextView to the formatted text. (Because this is an extension function on TextView, you can directly access the text property.)
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. To tell data binding about this binding adapter, annotate the function with @BindingAdapter.
  2. This function is the adapter for the sleepDurationFormatted attribute, so pass sleepDurationFormatted as an argument to @BindingAdapter.
@BindingAdapter("sleepDurationFormatted")
  1. The second adapter sets the sleep quality based on the value in a SleepNight object. Create another extension function called setSleepQualityString() on TextView, and pass in a SleepNight.
  2. In the body, bind the data to the view as you did in ViewHolder.bind(). Call convertNumericQualityToString and set the text.
  3. Annotate the function with @BindingAdapter("sleepQualityString").
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. We need a third binding adapter that sets the image on an image view. Create the third extension function on ImageView, call setSleepImage, and use the code from ViewHolder.bind(), as shown below.
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

You may have to import convertDurationToFormatted and convertNumericQualityToString.

Step 2: Update SleepNightAdapter

  1. Open SleepNightAdapter.kt.
  2. Delete everything in the bind() method, because you can now use data binding and your new adapters to do this work for you.
fun bind(item: SleepNight) {
}
  1. Inside bind(), assign sleep to item, because you need to tell the binding object about your new SleepNight.
binding.sleep = item
  1. Below that line, add binding.executePendingBindings(). This call is an optimization that asks data binding to execute any pending bindings right away. It's always a good idea to call executePendingBindings() when you use binding adapters in a RecyclerView, because it can slightly speed up sizing the views.
 binding.executePendingBindings()

Step 3: Add bindings to XML layout

  1. Open list_item_sleep_night.xml.
  2. In the ImageView, add an app attribute with the same name as the binding adapter that sets the image. Pass in the sleep variable, as shown below.

This property creates the connection between the view and the binding object, via the adapter. Whenever sleepImage is referenced, the adapter will adapt the data from the SleepNight.

app:sleepImage="@{sleep}"
  1. Now add a similar app attribute for the sleep_length and the quality_string text views. Whenever sleepDurationFormatted or sleepQualityString are referenced, the adapters will adapt the data from the SleepNight. Make sure to put each attribute in its respective TextView.
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. Run your app. It works exactly the same as it did before. The binding adapters take care of all the work of formatting and updating the views as the data changes, simplifying the ViewHolder and giving the code much better structure than it had before.

You've displayed the same list for the last few exercises. That's by design, to show you that the Adapter interface allows you to architect your code in many different ways. The more complex your code, the more important it becomes to architect it well. In production apps, these patterns and others are used with RecyclerView. The patterns all work, and each has its benefits. Which one you choose depends on what you are building.

Congrats! At this point you're well on your way to mastering RecyclerView on Android.

8. Solution code

Android Studio project: RecyclerViewDiffUtilDataBinding.

9. Summary

DiffUtil

  • RecyclerView has a class called DiffUtil which is for calculating the differences between two lists.
  • DiffUtil has a class called ItemCallBack that you extend in order to figure out the difference between two lists.
  • In the ItemCallback class, you must override the areItemsTheSame() and areContentsTheSame() methods.

ListAdapter

  • To get some list management for free, you can use the ListAdapter class instead of RecyclerView.Adapter. However, if you use ListAdapter you have to write your own adapter for other layouts, which is why this codelab shows you how to do it.
  • To open the intention menu in Android Studio, place the cursor on any item of code and press Alt+Enter (Option+Enter on a Mac). This menu is particularly helpful for refactoring code and creating stubs for implementing methods. The menu is context-sensitive, so you need to place the cursor exactly to get the correct menu.

Data binding

  • Use data binding in the item layout to bind data to the views.

Binding adapters

  • You previously used Transformations to create strings from data. If you need to bind data of different or complex types, provide binding adapters to help data binding use them.
  • To declare a binding adapter, define a method that takes an item and a view, and annotate the method with @BindingAdapter. In Kotlin, you can write the binding adapter as an extension function on the View. Pass in the name of the property that the adapter adapts. For example:
@BindingAdapter("sleepDurationFormatted")
  • In the XML layout, set an app property with the same name as the binding adapter. Pass in a variable with the data. For example:
.app:sleepDurationFormatted="@{sleep}"

10. Learn more

Udacity courses:

Android developer documentation:

Other resources:

11. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

Which of the following are necessary to use DiffUtil? Select all that apply.

▢ Extend the ItemCallBack class.

▢ Override areItemsTheSame().

▢ Override areContentsTheSame().

▢ Use data binding to track the differences between items.

Question 2

Which of the following are true about binding adapters?

▢ A binding adapter is a function annotated with @BindingAdapter.

▢ Using a binding adapter allows you to separate data formatting from the view holder.

▢ You must use a RecyclerViewAdapter if you want to use binding adapters.

▢ Binding adapters are a good solution when you need to transform complex data.

Question 3

When should you consider using Transformations instead of a binding adapter? Select all that apply.

▢ Your data is simple.

▢ You are formatting a string.

▢ Your list is very long.

▢ Your ViewHolder only contains one view.

12. Next codelab

Start the next lesson: