RecyclerView 作为Android 开发中最常用的开发组件,简单的静态页面,是不需要使用DiffUtils 的。为了提高RecyclerView的渲染性能,最容易想到的就是使用DiffUtils组件,一方面做到了只刷新某个变化了Item;另一方面通过DiffUtils 派发能够触发 RecyclerView默认动画效果,让界面更加优雅。
在前端各种双向绑定,数据驱动大行其道的今天,许多开发理念对Android同样适用;Kotlin 作为主要开发语言后,其各种语言特性,比如不可变数据,协程,Flow 与 Channel 让数据流组织和使用起来都更加清晰方便。
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,
private val newList: List
) : 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,
private val newList: List
) : DiffUtil.Callback() {
//.....
//重写 getChangePayload 方法
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val lh = oldList[oldItemPosition]
val rh = newList[newItemPosition]
//手动记录改变的数据
val payloads = mutableListOf()
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)
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){ 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上也是可以的。
先上使用方式
- 业务专注于数据和视图的映射关系,定义数据视图映射,无需定义对象来记录变动的字段。
val binders: DiffUpdater.Binder.() -> 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()) } }
- 在Diffcallback 中使用DiffUpdater生成字段变动对象。
private class SimpleDiffCallback(
private val oldList: List,
private val newList: List
) : DiffUtil.Callback() {
//.....
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
//记录变动
return DiffUpdater.createPayload(oldList[oldItemPosition],newList[newItemPosition])
}
}
- 在 onBindViewHolder,使用 DiffUpdater.Payload#dispatch 方法触发视图更新。
override fun onBindViewHolder(holder: CarViewHolder,position: Int,payloads: MutableList){ if (payloads.isNotEmpty()) { for(payload in payloads){ if(payload is DiffUpdater.Payload<*>){ //触发视图更新 (payload as DiffUpdater.Payload ).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(
val newState: T,
val changed: List>,
)
class Binder {
val handlers = mutableMapOf, (VH, Any?) -> Unit>()
infix fun KProperty1.onChange(action: (R) -> Unit) {
handlers[this] = action as (VH, Any?) -> Unit
}
}
fun Payload.dispatch(vh: VH, block: Binder.() -> Unit) {
val binder = Binder()
block(binder)
return doUpdate(vh,this, binder.handlers)
}
inline fun createPayload(lh: T, rh: T): Payload {
val clz = T::class as KClass
val changed: List = clz.memberProperties.filter {
it.get(lh as Any) != it.get(rh as Any)
}
return Payload(rh, changed as List>)
}
private fun doUpdate(
vh: VH,
payload: Payload,
handlers: Map, (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)
}
}
}



