Improving Application Performance with DiffUtil
The performance of a mobile application is one of the most important aspects in determining the user experience. Performance refers to the capability of an application to load and refresh data on various events.
<!--more-->
When you want to update data in a RecyclerView, you probably use notifyOnDataSetChanged()
or notifyOnItemPositionChanged()
methods. While these methods work well with small amounts of data, it becomes hectic for your app to update and fully reload huge amounts of data.
This easily leads to an ANR (App Not Responding) exception that annoys your application's users. An annoyed user will always give negative feedback!
With the use of DiffUtil
, we can easily handle changes in a dataset and instantly reflect them on a RecyclerView.
DiffUtil is a utility class that computes the variation between two sets of data and returns a list of update operations that change the old list to the new one. It uses the Eugene W. Myers' algorithm to figure out how many updates are essential to convert one data set to the other.
Since Myers' technique does not handle objects that have been moved, DiffUtil does a second attempt on the output to identify elements that have been relocated. This saves time and the device's resources while providing a better experience for the user.
Table of Contents
- Prerequisites
- Building an Android project
- Enabling viewBinding
- Setting up a RecyclerView
- Model Application data
- Creating DiffUtil Callback class
- Understanding the DiffUtil callbacks
- Attaching DiffUtil to an Adapter
- Utilizing the Adapter
- Testing and debugging the app
- Conclusion
Prerequisites
This article assumes that you have prior knowledge on:
- Creating Android apps using Android Studio.
- Basics of the Kotlin programming language.
- Working with the imperative paradigm in Android - using
XML
and view groups such as Constraint Layout. - ViewBinding and/or DataBinding.
- The basics of an Android Recyclerview.
Building an Android project
Open Android Studio and create a project with a template of your choice. Make sure that you select an API version not less than 16 so as to make your app compatible with DiffUtil.
Enabling viewBinding
ViewBinding allows us to access views and viewGroups from XML
layout files using their respective binding classes.
// in the build.gradle file (module level)
android{
buildFeatures{
viewBinding true
}
}
Setting up RecyclerView
At this point, we're going to create a RecyclerView and set it up with its core components.
User Interface
Open your XML
layout file and paste the following code.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--NOTE: This file is named activity_main.xml-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<Button
android:id="@+id/btnUpdate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/update_data"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/recyclerview"
app:layout_constraintStart_toStartOf="@id/recyclerview"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.96" />
</androidx.constraintlayout.widget.ConstraintLayout>
This includes a button that when clicked, will perform an update operation on the Recyclerview.
Row item
This is the unit building block that determines the data representation format in a Recyclerview. It also takes after the data models in the app for compatibility and necessity purposes.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp">
<!--NOTE: This file is named student_card.xml-->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<TextView
android:id="@+id/studentId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.25"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="00" />
<TextView
android:id="@+id/studentName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.25"
app:layout_constraintStart_toEndOf="@id/studentId"
app:layout_constraintTop_toTopOf="parent"
tools:text="Example Name" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
Here, we've created a student item card that holds two textViews.
Data model for the App
A dataset defines the type of data objects that the adapter works with. Create a Koltin data class named Student
and paste the following.
data class Student(
val id: String,
val name: String
)
Recyclerview Adapter
An adapter does the hard task of connecting the dataset to a Recyclerview. Create a Kotlin class named StudentAdapter
and paste the following.
class StudentAdapter : RecyclerView.Adapter<StudentAdapter.CardViewHolder>() {
private var oldList = emptyList<Student>()
inner class CardViewHolder(val card: StudentCardBinding) : RecyclerView.ViewHolder(card.root)
override fun getItemCount(): Int = oldList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
return CardViewHolder(
StudentCardBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: CardViewHolder, position: Int) {
holder.card.apply {
studentId.text = oldList[position].id
studentName.text = oldList[position].name
}
}
}
We'll later use this adapter to implement DiffUtil callbacks.
Creating DiffUtil Callback class
Now, proceed to create a Kotlin class named MyDiffUtil
and paste the following.
class MyDiffUtil(
private val oldList: List<Student>,
private val newList: List<Student>
) : DiffUtil.Callback() { // extending the Callback class
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return when {
oldList[oldItemPosition].id == newList[newItemPosition].id -> true
oldList[oldItemPosition].name == newList[newItemPosition].name -> true
else -> false
}
}
}
Understanding the DiffUtil callbacks
getOldListSize
returns the size of the old list passed in the constructor of the class.getNewListSize
returns the size of the new or updated list passed in the constructor.areItemsTheSame
returns a boolean value, i.e, if items at corresponding positions are the same or not.areContentsTheSame
checks if the content of items at corresponding positions is the same.
NOTE: Both oldList and newList should hold elements of the same type.
Attaching DiffUtil to an Adapter
Navigate to the StudentAdapter.kt
file and create a public function that we'll use to dispatch updates to the adapter.
fun setData(newList: List<Student>) {
val diffUtil = MyDiffUtil(oldList, newList)
val diffUtilResults = DiffUtil.calculateDiff(diffUtil)
oldList = newList
diffUtilResults.dispatchUpdatesTo(this)
}
Utilizing the Adapter
The adapter is ready to be used. Attach it to the Recyclerview as shown below.
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
private val studentAdapter: StudentAdapter = StudentAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding!!.root)
updateDataWhenClicked()
binding!!.recyclerview.apply {
val newList: List<Student> = listOf(
Student("1", "Milan Gans"),
Student("2", "Renda Slaugh"),
Student("3", "Basil Levison"),
Student("4", "Tina Travers"),
)
adapter = studentAdapter.also { adapter ->
adapter.setData(newList)
}
}
}
private fun updateDataWhenClicked() {
binding!!.btnUpdate.setOnClickListener {
val newList: List<Student> = listOf(
Student("1", "Milan Gans"),
Student("2", "Renda Slaugh"),
Student("3", "Basil Levison"),
Student("4", "Tina Travers"),
Student("5", "Lil Mosey"),
Student("6", "Maurine Alexa"),
)
studentAdapter.setData(newList)
}
}
}
Here we've initialized the adapter with a dummy list. When the update button is clicked, the RecyclerView is updated with a new dummy list.
TIP: When working on a production project, you should get data from a database, preferably room database or a remote database.
Test the App
After running the App, you should see something similar to this;
Notice that only the new items are updated. This way, the device's resources such as battery and memory are saved. As a result, the users have a quick and seamless experience.
Conclusion
That's it! You can now boost your app performance using DiffUtil.
For further reading, release notes, or advanced implementation of DiffUtil, please refer to the official documentation or this sample project.
The source code for this tutorial can be found here.
Happy Coding!
Peer Review Contributions by: Peter Kayere