This is the 14th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

Preface:

As the most commonly used development component in Android development, RecyclerView, a simple static page, does not need to use DiffUtils. In order to improve the rendering performance of RecyclerView, the most easy to think of is to use DiffUtils components, on the one hand, only refresh a changed Item; On the other hand, distribution through DiffUtils can trigger RecyclerView default animation effect, so that the interface is more elegant. In today’s world of bidirectional binding and data-driven front-end, many of the development concepts are applicable to Android as well; With Kotlin as the main development language, features such as immutable data, coroutines, flows, and channels make it easier to organize and use data flows.

A simple use of DiffUtils

DiffUtils is also easy to use. Simply pass in a DiffCallback and override a few of its methods. DiffUtils compares the differences between the old and new data sets and automatically triggers Adapter additions, deletions, and changes based on the differences. This is the way we use it most often in apps.

The following examples all use the Car type as the data class.

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

With Callback wrapped, basically two lines of code can implement the adapter add, delete, change distribution logic

val diffResult = DiffUtil.calculateDiff(SimpleDiffCallback(oldList, NewList) oldList. The 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] }Copy the code

High-order usage, payload using DiffUtils

The usage in the previous section is sufficient for general usage scenarios, but when an Item is changed, the entire Item is still refreshed, and the data still needs to be rebound to the view. To solve this problem, We generally need to override the getChangePayload method of DiffUtil.Callback.

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

Common usage

private class SimpleDiffCallback( private val oldList: List<Car>, private val newList: List<Car> ) : DiffUtil.Callback() { //..... Override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {val lh = oldList[oldItemPosition] val rh = newList[newItemPosition] // 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() }Copy the code

In this way, from the Adapter onBindViewHolder, we can easily retrieve a specific change in the data and update it to the view accordingly.

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)
    }
}
Copy the code

It is not difficult to use, mainly because there is a lot of template code, how to make the business logic more focused and view update through simple encapsulation.

DiffUtils meets Kotlin, a more elegant View partial refresh scheme

All of the data classes used in this article are Kotlin’s data classes, although simple changes to apply to Java are also possible.

How to use first

  1. The business focuses on the mapping between data and views, defining data view mappings without defining objects to record changing 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())
    }
}
Copy the code
  1. Use DiffUpdater in Diffcallback to generate the field change object.
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])
    }
}
Copy the code
  1. On onBindViewHolder, use the Diffupdater.payload #dispatch method to trigger the view update.
override fun onBindViewHolder(holder: CarViewHolder,position: Int,payloads: MutableList<Any? >){if(payload.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) } }Copy the code

In use, you can focus more on binding logic with data and views.

The appendix

Because you’re using Kotlin reflection, remember to add dependencies.

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

Finally, the 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) } } }Copy the code

Happy Ending.