当前位置: 首页 > 工具软件 > Diffutils > 使用案例 >

DiffUtils 遇到 Kotlin,榨干视图局部刷新的最后一滴性能

卫诚
2023-12-01

前言:

RecyclerView 作为Android 开发中最常用的开发组件,简单的静态页面,是不需要使用DiffUtils 的。为了提高RecyclerView的渲染性能,最容易想到的就是使用DiffUtils组件,一方面做到了只刷新某个变化了Item;另一方面通过DiffUtils 派发能够触发 RecyclerView默认动画效果,让界面更加优雅。 在前端各种双向绑定,数据驱动大行其道的今天,许多开发理念对Android同样适用;Kotlin 作为主要开发语言后,其各种语言特性,比如不可变数据,协程,Flow 与 Channel 让数据流组织和使用起来都更加清晰方便。

DiffUtils 的简单使用

DiffUtils 的使用起来也很简单,只需要简单的传入一个DiffCallback,重写其中的几个方法,DiffUtils 就能对比出新旧数据集差异,根据差异内容自动触发Adapter 的 增删改 通知,这也是我们在App 中最常用的使用方法。

在下面的示例中都使用Car类型作为数据类。

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

把Callback继续封装下,基本两行代码就可以实现adapter增删改的派发逻辑

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

//重写一个Callback 实现
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]
}

高阶使用,使用DiffUtils 的 payload

上一节的使用方式满足一般的使用场景已经足够了,但是在一个Item Change 时仍然会刷新整个Item,数据仍然需要重新绑定一遍视图,为了解决这个问题,我们一般需要重写 DiffUtil.Callback 的 getChangePayload 方法。

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

通常的使用方式

private class SimpleDiffCallback(
    private val oldList: List<Car>,
    private val newList: List<Car>
) : DiffUtil.Callback() {
    //.....
    
    //重写 getChangePayload 方法
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val lh = oldList[oldItemPosition]
        val rh = newList[newItemPosition]
        //手动记录改变的数据
        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()
}

这样子在 Adapter 的 onBindViewHolder 中,我们就可以方便取到数据中具体某一项的变化,并针对性的更新到视图中。

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){ 
                    // 更新视图数据
                    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)
    }
}

使用起来繁琐单并不困难,主要是模板代码比较多,怎么通过简单的封装,让业务逻辑更加专注与视图更新呢。

DiffUtils 遇到Kotlin,更优雅的View局部刷新方案

本文中使用的都是Kotlin的数据类,当然简单改动后应用到Java上也是可以的。

先上使用方式

  1. 业务专注于数据和视图的映射关系,定义数据视图映射,无需定义对象来记录变动的字段。
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. 在Diffcallback 中使用DiffUpdater生成字段变动对象。
private class SimpleDiffCallback(
    private val oldList: List<Car>,
    private val newList: List<Car>
) : DiffUtil.Callback() {
    //.....
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        //记录变动
        return DiffUpdater.createPayload(oldList[oldItemPosition],newList[newItemPosition])
    }
}
  1. 在 onBindViewHolder,使用 DiffUpdater.Payload#dispatch 方法触发视图更新。
override fun onBindViewHolder(holder: CarViewHolder,position: Int,payloads: MutableList<Any?>){
    if (payloads.isNotEmpty()) {
        for(payload in payloads){
            if(payload is DiffUpdater.Payload<*>){
                //触发视图更新
                (payload as DiffUpdater.Payload<Car>).dispatch(holder,binders)
            }
        }
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

在使用上可以更加专注与数据和视图的绑定逻辑。

附录

因为使用了Kotlin反射,记得加上相关依赖。

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

最后附上完整的DiffUpater。

@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)
        }
    }
}

Happy Ending.

 类似资料: