前言:
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上也是可以的。
先上使用方式
- 业务专注于数据和视图的映射关系,定义数据视图映射,无需定义对象来记录变动的字段。
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())
}
}
- 在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])
}
}
- 在 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)
}
}
}