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

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DiffUtils 遇到 Kotlin,榨干视图局部刷新的最后一滴性能相关的知识,希望对你有一定的参考价值。

前言:

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)
        
    

以上是关于DiffUtils 遇到 Kotlin,榨干视图局部刷新的最后一滴性能的主要内容,如果未能解决你的问题,请参考以下文章

ListView 数据未在 Kotlin 中显示

docker下的spark集群,调整参数榨干硬件

Kotlin / 迁移到视图绑定

如何一条命令,榨干机器的所有内存?

如何使用 DiffUtils 更新 RecyclerView 中的列表?

Linux From Scratch(LFS11.0)构建 LFS 系统 - Diffutils-3.8