DiffUtils encountered Kotlin and drained the last drop of performance of local refresh of the view

preface:

RecyclerView, as the most commonly used development component in Android development, does not need to use DiffUtils for simple static pages. In order to improve the rendering performance of RecyclerView, the easiest thing to think of is to use the DiffUtils component. On the one hand, only a changed Item is refreshed; On the other hand, distribution through DiffUtils can trigger the default animation effect of RecyclerView, making the interface more elegant.
Today, when a variety of two-way binding and data-driven are popular at the front end, many development concepts are also applicable to Android; After Kotlin is the main development language, its various language features, such as immutable data, collaborative process, Flow and Channel, make the organization and use of data Flow more clear and convenient.

Simple use of DiffUtils

DiffUtils is also very simple to use. Simply pass in a DiffCallback and rewrite several methods. DiffUtils can compare the differences between the old and new data sets and automatically trigger the addition, deletion and modification notification of the Adapter according to the difference content. This is also the most commonly used method in the App.

In the following examples, the Car type is used as the data class.

data class Car(val band: String, val color: Int, val image: String, val price: Int) {

Continue to encapsulate the Callback, and the basic two lines of code can realize the distribution logic of adding, deleting and modifying the adapter

val diffResult = DiffUtil.calculateDiff(SimpleDiffCallback(oldList, newList))
oldList.clear()
oldList.addAll(data)
diffResult.dispatchUpdatesTo(adapter)

//Rewrite a Callback implementation
class SimpleDiffCallback(
    private val oldList: List<RenderData>,
    private val newList: List<RenderData>
) : DiffUtil.Callback() {
    override fun areItemsTheSame(lh: Int, rh: Int) = from[lh].band == to[rh].band
    override fun getOldListSize(): Int = oldList.size
    override fun getNewListSize(): Int = newList.size
    override fun areContentsTheSame(lh: Int, rh: Int) = from[lh] == to[rh]
}

For higher-order use, use the payload of DiffUtils

The usage in the previous section is enough to meet the general usage scenarios, but the entire Item will still be refreshed when an Item is changed, and the data still needs to be rebinding the view. In order to solve this problem, we generally need to rewrite the getChangePayload method of DiffUtil.Callback.

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {

Usual usage

private class SimpleDiffCallback(
    private val oldList: List<Car>,
    private val newList: List<Car>
) : DiffUtil.Callback() {
    //.....
    
    //Override the getChangePayload method
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val lh = oldList[oldItemPosition]
        val rh = newList[newItemPosition]
        //Manually record changed data
        val payloads = mutableListOf<CarChange>()
        if (lh.image != rh.image) {
            payloads.add(CarChange.ImageChange(rh.image))
        }
        if (lh.price != rh.price) {
            payloads.add(CarChange.PriceChange(rh.price))
        }
        if (lh.color != rh.color) {
            payloads.add(CarChange.ColorChange(rh.color))
        }
        return CarPayLoad(payloads)
    }
}

data class CarPayLoad(val changes: List<CarChange>)
sealed class CarChange {
    data class ImageChange(val image: String): CarChange()
    data class PriceChange(val price: Int): CarChange()
    data class ColorChange(val color: Int): CarChange()
}

In this way, in the onBindViewHolder of the Adapter, we can easily get the change of a specific item in the data and update it to the view.

override fun onBindViewHolder(holder: CarViewHolder,position: Int,payloads: MutableList<Any?>){
    if (payloads.isNotEmpty()) {
        for (payload in payloads) {
            if (payload is CarPayLoad) {
                for(change in payload.changes){ 
                    // Update view data
                    when (change) {
                        is CarChange.ColorChange -> holder.colorIv.setColor(change.color)
                        is CarChange.ImageChange -> holder.imageIv.setImaggSrc(change.image)
                        is CarChange.PriceChange -> holder.priceTv.setText(change.price)
                    }
                }
            }
        }
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

It's not difficult to use it. The main reason is that there are a lot of template code. How can we make the business logic more focused and view updated through simple encapsulation.

DiffUtils encounters Kotlin, a more elegant View local refresh scheme

Kotlin's data classes are used in this article. Of course, it is also possible to apply them to Java after simple changes.
First use

  1. The business focuses on the mapping relationship between data and view, defines data view mapping, and does not need to define objects to record changed fields.
val binders: DiffUpdater.Binder<Car>.() -> Unit = {
    Car::color onChange { (vh,color) ->
        vh.colorIv.setColor(color)
    }
    
    Car::image onChange { (vh,image) ->
        vh.imageIv.setImageSrc(image)
    }
    
    Car::price onChange { (vh,price) ->
        vh.priceTv.setText(price.toString())
    }
}
  1. Use DiffUpdater to generate field change objects in Diffcallback.
private class SimpleDiffCallback(
    private val oldList: List<Car>,
    private val newList: List<Car>
) : DiffUtil.Callback() {
    //.....
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        //Record changes
        return DiffUpdater.createPayload(oldList[oldItemPosition],newList[newItemPosition])
    }
}
  1. In the onBindViewHolder, use the DiffUpdater.Payload#dispatch method to trigger the view update.
override fun onBindViewHolder(holder: CarViewHolder,position: Int,payloads: MutableList<Any?>){
    if (payloads.isNotEmpty()) {
        for(payload in payloads){
            if(payload is DiffUpdater.Payload<*>){
                //Trigger view update
                (payload as DiffUpdater.Payload<Car>).dispatch(holder,binders)
            }
        }
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

In use, you can pay more attention to the binding logic with data and view.

#Appendix

Because Kotlin reflection is used, remember to add related dependencies.

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect:${version}"
}

Finally, a complete DiffUpater is attached.

@Suppress("UNCHECKED_CAST")
object DiffUpdater {

    data class Payload<T>(
        val newState: T,
        val changed: List<KProperty1<T, *>>,
    )

    class Binder<T, VH : RecyclerView.ViewHolder> {
        val handlers = mutableMapOf<KProperty1<T, *>, (VH, Any?) -> Unit>()

        infix fun <R> KProperty1<T, R>.onChange(action: (R) -> Unit) {
            handlers[this] = action as (VH, Any?) -> Unit
        }
    }

    fun <T, VH : RecyclerView.ViewHolder> Payload<T>.dispatch(vh: VH, block: Binder<T,VH>.() -> Unit) {
        val binder = Binder<T,VH>()
        block(binder)
        return doUpdate(vh,this, binder.handlers)
    }

    inline fun <reified T> createPayload(lh: T, rh: T): Payload<T> {
        val clz = T::class as KClass<Any>
        val changed: List<KProperty1<Any, *>> = clz.memberProperties.filter {
            it.get(lh as Any) != it.get(rh as Any)
        }
        return Payload(rh, changed as List<KProperty1<T, *>>)
    }

    private fun <T,VH : RecyclerView.ViewHolder> doUpdate(
        vh: VH,
        payload: Payload<T>,
        handlers: Map<KProperty1<T, *>, (VH,Any?) -> Unit>,
    ) {
        val (state, changedProps) = payload
        for (prop in changedProps) {
            val handler = handlers[prop]
            if (handler == null) {
                print("not handle with ${prop.name} change.")
                continue
            }
            val newValue = prop.get(state)
            handler(vh,newValue)
        }
    }
}

Keywords: Android Design Pattern kotlin

Added by AMV on Mon, 22 Nov 2021 11:22:31 +0200