
Using DiffUtil
Forever forget about such a great thing like DiffUtil.
Let’s create the simplest Recycler with an adapter and a list of poets.
class MainAdapter(var items: List<Author>): RecyclerView.Adapter<MainAdapter.MainHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MainHolder(LayoutInflater.from(parent.context).inflate(R.layout.main_item, parent, false))
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: MainHolder, position: Int) = holder.bind(items[position])
inner class MainHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(author: Author) {
with(itemView) {
mainItemId.text = author.id.toString()
mainItemFirstName.text = author.firstName
mainItemLastName.text = author.lastName
mainItemBirthdayYear.text = author.birthdayYear.toString()
}
}
}
}
<?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=".ui.activity.MainActivity"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/buttonUpdate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/main_item"
/>
<Button
android:id="@+id/buttonUpdate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="update"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<?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:layout_margin="10dp"
>
<TextView
android:id="@+id/mainItemId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="id"
/>
<TextView
android:id="@+id/mainItemFirstName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="FirstName"
/>
<TextView
android:id="@+id/mainItemLastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/mainItemFirstName"
tools:text="LastName"
android:layout_marginStart="5dp"
/>
<TextView
android:id="@+id/mainItemBirthdayYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/mainItemLastName"
tools:text="Year"
android:layout_marginStart="5dp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
data class Author(
val id: Long,
val firstName: String,
val lastName: String,
val birthdayYear: Int
)
ext {
SupportLibraryVersion = '1.0.0'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.appcompat:appcompat:$SupportLibraryVersion"
implementation "androidx.recyclerview:recyclerview:$SupportLibraryVersion"
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.0-beta01'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-beta01'
}
Now it’s time to create MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val author1 = Author(1L, "Александр", "Пушкин", 1799)
val author2 = Author(2L, "Михаил", "Лермонтов", 1814)
val author3 = Author(3L, "Александр", "Блок", 1880)
val author4 = Author(4L, "Николай", "Некрасов", 1821)
val author5 = Author(5L, "Фёдор", "Тютчев", 1803)
val author6 = Author(6L, "Сергей", "Есенин", 1895)
val author7 = Author(7L, "Владимир", "Маяковский", 1893)
val mainAdapter = MainAdapter(listOf(author1, author2, author3, author4, author5, author6, author7))
mainRecycler.adapter = mainAdapter
}
}
You can start it – everything will work fine. But wait. Add a button handler:
buttonUpdate.setOnClickListener {
val newItems = listOf(
author1,
author2.copy(firstName = author2.firstName.toUpperCase(), lastName = author2.lastName.toUpperCase()),
Author(8L, "Самуил", "Маршак", 1887),
author3,
author4,
author5,
author7
)
mainAdapter.items = newItems
mainAdapter.notifyDataSetChanged()
}
Now, when you click on the Update button, the list will change. This will redraw all the elements of the list. This is easy to verify if you embed the logger in the adapter’s bind() method. But what if there are a lot of elements? Has only one or two changed? This is where DiffUtil comes in handy. First, I need a helper class that will do the main magic:
class AuthorDiffUtilCallback(private val oldList: List<Author>, private val newList: List<Author>): DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition].id == newList[newItemPosition].id
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition] == newList[newItemPosition]
}
As you can see from the code, nothing complicated: it receive old and new lists, has two methods for issuing the size of the sheets and two for comparison – by id (for example) and by content. The first comparison method is needed to determine if that element is at all. If their id is different, then you do not need to check further, this is definitely not the same object. But if the id is the same, then you can already connect “big guns” – a method that compares by content, which is much more expensive in terms of resources, especially on large complex objects.
Now you can change the method on the button:
buttonUpdate.setOnClickListener {
val newItems = listOf(
author1,
author2.copy(firstName = author2.firstName.toUpperCase(), lastName = author2.lastName.toUpperCase()),
Author(8L, "Самуил", "Маршак", 1887),
author3,
author4,
author5,
author7
)
val authorDiffUtilCallback = AuthorDiffUtilCallback(mainAdapter.items, newItems)
val authorDiffResult = DiffUtil.calculateDiff(authorDiffUtilCallback)
mainAdapter.items = newItems
authorDiffResult.dispatchUpdatesTo(mainAdapter)
}
I can analyze in more detail. First I create a callback, on which comparisons will be made, I push both lists into it. Then, using DiffUtil.calculateDiff() method, which compare lists. And as a result, I call RESULT.dispatchUpdatesTo(ADAPTER) method, which will do everything, and even with beautiful animation. You can run and compare.
Code on my GitHub.