Android 列表差量更新与 DiffUtil
Android 列表差量更新与 DiffUtil
列表更新最直接的做法是 notifyDataSetChanged,但全量刷新会丢失输入焦点、动画和滚动状态,也会让所有可见 Item 重新绑定。DiffUtil 通过对比新旧数据计算最小变化集,让 RecyclerView 只更新真正变化的项。
一、全量刷新的问题
notifyDataSetChanged 让 RecyclerView 忘记当前状态,只能重新绑定所有可见 ViewHolder。
fun updateItems(items: List<Order>) {
this.items = items
notifyDataSetChanged()
}
列表较长、Item 布局复杂或包含图片时,全量刷新会造成明显卡顿,也会打断用户正在进行的操作。
二、DiffUtil 计算最小差异
DiffUtil.Callback 负责定义两个列表的比较规则。
class OrderDiffCallback(
private val oldList: List<Order>,
private val newList: List<Order>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].id == newList[newPos].id
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
areItemsTheSame 判断是否是同一条业务数据,areContentsTheSame 判断内容是否一致。两个方法职责不能混淆。
三、使用 ListAdapter 简化流程
ListAdapter 内部持有列表并处理差量计算,适配器只实现比较规则。
class OrderAdapter : ListAdapter<Order, OrderViewHolder>(ORDER_DIFF) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
return OrderViewHolder.inflate(parent)
}
override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
holder.bind(getItem(position))
}
companion object {
private val ORDER_DIFF = object : DiffUtil.ItemCallback<Order>() {
override fun areItemsTheSame(oldItem: Order, newItem: Order): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Order, newItem: Order): Boolean {
return oldItem == newItem
}
}
}
}
数据更新通过 submitList 完成,不要在外部再维护一份列表引用。
四、数据模型应正确实现相等
如果数据类包含不稳定字段,例如每次请求都变化的时间戳,内容比较会永远认为不同,导致无意义的刷新。
data class Order(
val id: String,
val title: String,
val amount: Long,
val status: String
)
数据模型中只保留影响展示的稳定字段。埋点字段、调试信息等不应参与内容比较。
五、列表更新必须提交新集合
submitList 需要新的列表实例。直接修改原列表后再次提交同一引用,无法计算差异。
fun onOrdersLoaded(orders: List<Order>) {
adapter.submitList(orders.toList())
}
ViewModel 中同样应保持列表不可变,更新时创建新集合,避免多处引用同一个可变列表。
六、复杂变化使用 Payload
只有状态或局部字段变化时,Payload 可以避免整项重绑,只更新变化区域。
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
val oldItem = oldList[oldPos]
val newItem = newList[newPos]
return if (oldItem.amount == newItem.amount) {
OrderPayload.StatusChanged
} else {
null
}
}
使用 Payload 后需重写带 Payload 的 onBindViewHolder,只更新状态控件。Payload 的粒度不要过细,否则维护成本会超过收益。
七、后台计算大列表差异
DiffUtil 计算在列表规模较大时可能耗时。submitList 支持在后台计算,完成后再提交到主线程。
adapter.submitList(newItems, commitCallback)
ListAdapter 已封装后台计算能力。如果手写 DiffUtil.calculateDiff,应将计算结果在主线程分发,不要在主线程处理大列表。
八、局部刷新与滚动位置
差量更新会自动保留未变化项的 ViewHolder,用户滚动位置通常也能保持。但插入头部或删除当前项时,需要确认产品期望的滚动行为。
adapter.submitList(newItems) {
layoutManager.scrollToPosition(0)
}
不要为了强制刷新而插入变化的临时字段。这会让差异计算失效,也容易造成闪烁。
九、避免在绑定阶段做重活
差量更新减少了重绑次数,但单个 Item 的绑定仍应保持轻量。
fun bind(order: Order) {
titleView.text = order.title
amountView.text = priceFormatter.format(order.amount)
}
格式化、对象创建和图片加载应使用缓存或组件库能力。绑定阶段不要发起同步请求或复杂数据转换。
十、测试更新场景
差量更新容易在边界场景出错,应覆盖:
- 新增、删除、移动同时发生。
- 头部插入和尾部追加。
- 同一项状态局部变化。
- 快速连续提交列表。
- 空列表与恢复列表。
- 滚动过程中刷新。
总结
列表差量更新的关键是比较规则准确。areItemsTheSame 使用稳定业务标识,areContentsTheSame 使用影响展示的字段。数据保持不可变并提交新集合,大列表差异计算交给后台,局部变化可用 Payload。目标是减少重绑、保留状态和动画,让列表刷新对用户透明。
- 点赞
- 收藏
- 关注作者
评论(0)