18、ListAdapter 简化 DiffUtil:使用 ListAdapter 替代传统 Adapter
说实话,刚接触 RecyclerView 那会儿,我最头疼的就是刷新列表。
每次数据变了,要么 notifyDataSetChanged() 一把梭,要么自己写 DiffUtil 算差异。前者性能拉胯,后者代码量爆炸。直到我发现了 ListAdapter——嗯,这玩意儿简直就是为「懒且追求性能」的开发者量身定做的。
传统 Adapter 的痛点
咱们先回顾一下,传统写法有多折腾。
你写一个 RecyclerView.Adapter,数据变了,你得手动调用 notifyItemInserted()、notifyItemRemoved() 这些方法。如果数据是整体替换,你还得自己写 DiffUtil 去算哪些变了。
我在项目中遇到过这么个场景:聊天列表,每秒钟可能来好几条新消息。如果用 notifyDataSetChanged(),整个列表闪一下,用户体验极差。用 DiffUtil 吧,每次都得写一个 Callback 类,代码重复得让人想吐。
核心问题:传统 Adapter 把「数据变化检测」和「UI 刷新」混在了一起。你既要管数据,又要管动画,还要管性能。
ListAdapter 是什么?
ListAdapter 是 Android 官方提供的一个抽象类,它继承自 RecyclerView.Adapter。
说白了,它把 DiffUtil 的计算逻辑内置到了 Adapter 里。你只需要告诉它「新旧数据长啥样」,它自动帮你算差异、做动画。
它的核心就两个东西:
- AsyncDifferConfig:配置 DiffUtil 的异步计算线程
- AsyncListDiffer:真正干活的,在后台线程算差异,主线程刷新
你想想看,以前你得自己 new 一个 DiffUtil.Callback,现在 ListAdapter 帮你包好了。你只需要重写 areItemsTheSame() 和 areContentsTheSame() 两个方法就行。
实战:用 ListAdapter 改写传统 Adapter
我直接上代码,咱们对比着看。
传统写法(自己写 DiffUtil)
class OldAdapter : RecyclerView.Adapter<OldAdapter.ViewHolder>() {
private var dataList: List<Item> = emptyList()
fun submitList(newList: List<Item>) {
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = dataList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldPos: Int, newPos: Int) =
dataList[oldPos].id == newList[newPos].id
override fun areContentsTheSame(oldPos: Int, newPos: Int) =
dataList[oldPos] == newList[newPos]
})
dataList = newList
diffResult.dispatchUpdatesTo(this)
}
// ... 其他代码
}
这段代码看着还行?但每次都要写 Callback,而且 dispatchUpdatesTo 必须在主线程调用。如果数据量大,主线程卡顿是必然的。
ListAdapter 写法
class NewAdapter : ListAdapter<Item, NewAdapter.ViewHolder>(object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// 和传统写法一样
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.bind(item)
}
}
看到区别了吗?
- ListAdapter 的构造函数直接接收一个
DiffUtil.ItemCallback - 你不需要自己维护
dataList,内部已经帮你存了 - 调用
submitList()时,差异计算自动在后台线程执行 - 刷新动画自动触发,你什么都不用管
个人习惯:我一般把 DiffUtil.ItemCallback 写成 companion object 里的单例,避免每次 new Adapter 都重新创建。尤其是列表页来回切换的时候,能省一点内存。
ListAdapter 的异步机制
为什么说 ListAdapter 性能好?因为它用了 AsyncListDiffer。
当你调用 submitList() 时,它会把新旧数据扔到后台线程去算 DiffUtil。算完之后,再切回主线程调用 notifyItemRangeInserted() 这些方法。
这就意味着:
- 主线程不卡顿
- 动画是精准的(不会整个列表闪一下)
- 你甚至可以在短时间内多次调用
submitList(),它会自动合并最后一次的更新
我曾经踩过的坑:有一次我连续调了三次 submitList(),结果列表只刷新了最后一次的数据。后来看了源码才发现,ListAdapter 内部有一个「最大待处理任务数」的限制。如果你提交得太快,它会丢弃中间的结果,只保留最新的。所以,如果你需要保证每次提交都生效,记得在提交前判断一下数据是否真的变了。
核心知识体系
下面这张图,是我自己总结的 ListAdapter 工作流程。你看完应该就全明白了。
什么时候用 ListAdapter?
不是所有场景都适合用 ListAdapter。我个人的经验是:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 列表数据频繁变化(聊天、直播) | ListAdapter | 异步计算,不卡主线程 |
| 列表数据一次性加载(设置页) | 传统 Adapter 或 ListAdapter | 数据变化少,两者差别不大 |
| 列表需要手动控制动画 | 传统 Adapter | ListAdapter 的动画是自动的,不好精细控制 |
| 数据量极大(上万条) | ListAdapter + Paging | ListAdapter 本身不处理分页,需要配合 Paging3 |
一个小技巧:如果你用 ListAdapter 配合 LiveData 或 Flow,记得在 submitList() 之前做一次数据拷贝。因为 ListAdapter 内部是通过引用比较来判断数据是否变化的。如果你直接传同一个 List 对象,它可能认为数据没变,就不刷新了。
我一般这样写:adapter.submitList(newList.toList()),确保传进去的是一个新对象。
避坑指南
最后,分享几个我实际开发中遇到的坑:
- areItemsTheSame 别写错:这个方法是判断「是不是同一个数据项」,一般用 id 比较。如果你写成了内容比较,那动画就全乱了。
- areContentsTheSame 别偷懒:如果你只比较 id,不比较内容,那列表更新时就不会触发
onBindViewHolder。数据变了但 UI 没变,用户会以为 App 卡了。 - submitList 别传 null:ListAdapter 允许传 null,但传 null 会导致列表清空。如果你只是想刷新数据,传一个空列表
emptyList()更安全。
嗯,ListAdapter 其实没那么神秘。说白了,它就是官方帮你封装好的「带 DiffUtil 的 Adapter」。你只要把比较逻辑写好,剩下的交给它就行。
我个人现在写新项目,只要用到 RecyclerView,默认就用 ListAdapter。省心,性能也好。
公众号:蓝海资料掘金营,微信deep3321