I always forgot about DiffUtil. But it’s so beautiful!

Let’s take simple Recycler with Adapter and (in tradition) russian poets list.

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'
}

Теперь пора заняться MainActivity:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val author1 = Author(1L, "Alexander", "Pushkin", 1799)
        val author2 = Author(2L, "Michael", "Lernomontov", 1814)
        val author3 = Author(3L, "Alexander", "Blok", 1880)
        val author4 = Author(4L, "Nikolay", "Nekrasov", 1821)
        val author5 = Author(5L, "Teodor", "Tutchev", 1803)
        val author6 = Author(6L, "Serge", "Esenin", 1895)
        val author7 = Author(7L, "Vladimir", "Mayakovsky", 1893)

        val mainAdapter = MainAdapter(listOf(author1, author2, author3, author4, author5, author6, author7))
        mainRecycler.adapter = mainAdapter
    }
}

We can run it – all works fine. But! Let’s add button handler:

buttonUpdate.setOnClickListener {
    val newItems = listOf(
            author1,
            author2.copy(firstName = author2.firstName.toUpperCase(), lastName = author2.lastName.toUpperCase()),
            Author(8L, "Samuil", "Marshak", 1887),
            author3,
            author4,
            author5,
            author7
    )
    mainAdapter.items = newItems
    mainAdapter.notifyDataSetChanged()
}

Now, when we click Update button, list will changed. And ALL elements will be recreated an screen again. It can be confirmed, if we include logger in adapter bind() method. And what if there are a lot of elements? And only one or two changed? This is where DiffUtil comes in handy. For begining, I will need a helper class that will do the 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 can be seen from the code, nothing complicated: it takes the old and the new lists, have two methods for return size of lists and two for comparison – by id (for example) and by content. The first comparing method is needed to determine if that elements is even. If their id is different, then there is no need to check further, this is definitely not the same object. But if the id is the same, then you can already connect the “big guns” – a method that compares the content, which is much more expensive in terms of resources, especially on large complex objects.

Now we can change handler code:

buttonUpdate.setOnClickListener {
    val newItems = listOf(
            author1,
            author2.copy(firstName = author2.firstName.toUpperCase(), lastName = author2.lastName.toUpperCase()),
            Author(8L, "Samuil", "Marshak", 1887),
            author3,
            author4,
            author5,
            author7
    )
    val authorDiffUtilCallback = AuthorDiffUtilCallback(mainAdapter.items, newItems)
    val authorDiffResult = DiffUtil.calculateDiff(authorDiffUtilCallback)
    mainAdapter.items = newItems
    authorDiffResult.dispatchUpdatesTo(mainAdapter)
}

I will analyze in more detail. First, I create callback, by which comparisons will be made, I put both lists into it. Now, with DiffUtil.calculateDiff()  help, compare lists. And call RESULT.dispatchUpdatesTo(ADAPTER) , on comparsion result, who will do everything himself, and also with beautiful animation. Now we can run it.

CU, Code on GitHub.

Leave a Reply

Close Menu