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.

Leave a Reply