为什么用差分(替代手工notify)
手工 notifyDataSetChanged():
-
全量重绑 → 闪烁/掉帧;
-
动画信息丢失(新增/删除/移动/改变都看不见);
-
大列表 UI 线程压力大。
DiffUtil 通过"旧列表 vs 新列表"最小编辑序列 (insert/remove/move/change)计算,后台线程完成,再把精准的 notifyItem* 派发给 RecyclerView,从而:
- 只更新改变的 item(配合 payload 可只改控件);
- 自然获得新增/删除/移动/改变动画;
- UI 线程工作量小,不卡。
A.DiffUtil.ItemCallback:写好这 3 个方法
kotlin
data class Article(
val id: Long, // 业务唯一ID
val title: String,
val liked: Boolean,
val likeCount: Int
)
object ArticleItemCallback : DiffUtil.ItemCallback<Article>() {
// 判定"是不是同一个实体"
override fun areItemsTheSame(old: Article, new: Article) =
old.id == new.id
// 判定"内容是否完全一致"(影响是否触发 onBind 或 change 动画)
override fun areContentsTheSame(old: Article, new: Article) =
old == new // data class 已经比较字段
// 可选:返回"变化的字段",支持 payload 局部刷新
override fun getChangePayload(old: Article, new: Article): Any? =
ArticlePayload(
title = new.title .takeIf { it != old.title },
liked = new.liked .takeIf { it != old.liked },
likeCount = new.likeCount .takeIf { it != old.likeCount },
).takeIf { it.hasAny() }
}
data class ArticlePayload(
val title: String? = null,
val liked: Boolean? = null,
val likeCount: Int? = null
) { fun hasAny() = title!=null || liked!=null || likeCount!=null }
口诀:ItemsSame 用 ID ,ContentsSame 用 equals ,Payload 用差异字段。
B.ListAdapter:官方推荐,最省心
ListAdapter 内部就用 AsyncListDiffer(后台线程 diff),你只管 submitList()。
kotlin
class ArticleAdapter :
ListAdapter<Article, ArticleVH>(ArticleItemCallback) {
init { setHasStableIds(true) } // 建议开稳定ID(见"进阶")
override fun getItemId(pos: Int) = getItem(pos).id
override fun onCreateViewHolder(p: ViewGroup, vt: Int): ArticleVH =
ArticleVH(ItemArticleBinding.inflate(LayoutInflater.from(p.context), p, false))
// 让无 payload 的重载也走 payload 分支,便于统一增量绑定
override fun onBindViewHolder(h: ArticleVH, pos: Int) =
onBindViewHolder(h, pos, emptyList())
override fun onBindViewHolder(h: ArticleVH, pos: Int, payloads: MutableList<Any>) {
val item = getItem(pos)
if (payloads.isNotEmpty()) {
val p = payloads.last() as ArticlePayload
p.title?.let { h.binding.title.text = it }
p.liked?.let {
h.binding.likeIcon.isSelected = it
// ...颜色/可点击状态等
}
p.likeCount?.let { h.binding.likeCount.text = it.toString() }
return
}
// 完整绑定(首次展示或未提供 payload)
h.bind(item)
}
}
// 使用:只改数据,交给差分
val adapter = ArticleAdapter()
recyclerView.adapter = adapter
adapter.submitList(newList) // 新旧列表可长可短、顺序可变
// adapter.submitList(list) { /* diff 应用完成后的回调,可滚动/埋点 */ }
要点
- 只传"新列表实例" :不要就地 mutate 原列表;否则 DiffUtil 无法正确比对。常用 newList = oldList.toMutableList().apply { ... }.
- submitList(null) 清空;submitList(emptyList()) 也可以。
- setHasStableIds(true) + getItemId():动画更准、复用更稳(见"进阶")。
C.AsyncListDiffer:保留你原有 Adapter,又想用差分
kotlin
class LegacyAdapter : RecyclerView.Adapter<ArticleVH>() {
private val differ = AsyncListDiffer(
this,
AsyncDifferConfig.Builder(ArticleItemCallback).build()
)
fun submit(list: List<Article>?) = differ.submitList(list)
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(p: ViewGroup, vt: Int) = ArticleVH(...)
override fun onBindViewHolder(h: ArticleVH, pos: Int) = h.bind(differ.currentList[pos])
// 要做 payload 增量,就覆写带 payload 的重载 + 在 ItemCallback.getChangePayload 里产出 payload
override fun onBindViewHolder(h: ArticleVH, pos: Int, payloads: MutableList<Any>) { ... }
}
适用:你已经有复杂的多类型 Adapter、不想改基类时;或要自定义 AsyncDifferConfig(提供你自己的线程池/MainThreadExecutor)。
D. 直接用DiffUtil.calculateDiff(理解原理/特殊场景)
当你要一次性替换数据且不想引入 ListAdapter:
kotlin
val old = items
val new = computeNewItems()
val cb = object : DiffUtil.Callback() {
override fun getOldListSize() = old.size
override fun getNewListSize() = new.size
override fun areItemsTheSame(o: Int, n: Int) = old[o].id == new[n].id
override fun areContentsTheSame(o: Int, n: Int) = old[o] == new[n]
override fun getChangePayload(o: Int, n: Int): Any? = ... // 可选
}
val diff = DiffUtil.calculateDiff(cb, detectMoves = true) // 可在后台线程跑
items = new
diff.dispatchUpdatesTo(adapter) // 在主线程应用
进阶:把差分威力拉满
1)稳定 ID(强烈建议)
-
adapter.setHasStableIds(true) + override getItemId() 返回业务唯一ID;
-
动画更精准(尤其是 move/change),复用更稳定、减少闪烁;
-
多类型时可用"高位分桶"避免冲突:(typeCode.toLong() shl 60) or id.
2)payload 局部刷新
- 在 ItemCallback.getChangePayload 返回变化字段 → onBind(payloads) 中只更新相关控件;
- 搭配关闭"细微 change 动画"可防止闪烁:
ini
(recyclerView.itemAnimator as? SimpleItemAnimator)
?.supportsChangeAnimations = false
3)并发与顺序
-
ListAdapter/AsyncListDiffer 会为每次 submitList 编号,旧 diff 结果会被丢弃,无需担心越界/乱序;
-
若要在"差分完成后"做滚动或埋点,用 submitList(list) { ... } 的 commitCallback。
4)与 Paging 3/ConcatAdapter
- Paging 的 PagingDataAdapter 继承自 ListAdapter,同一套写法(ItemCallback + payload + 稳定 ID);
- ConcatAdapter 里建议配置稳定 ID 模式:
scss
ConcatAdapter(
ConcatAdapter.Config.Builder()
.setStableIdMode(ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS)
.build(),
headerAdapter, bodyAdapter, footerAdapter
)
5)多类型列表
- areItemsTheSame 需要类型也一致(同 ID、不同类型算不同项);
- 可用密钥型 viewType 或 sealed class;ListAdapter 完全支持。
常见坑 & 速修
-
mutate 原列表 → diff 失效/更新不准
➜ 总是 传新列表实例;数据层用 不可变对象(data class)。
-
areItemsTheSame 写错(拿 position 或 equals)
➜ 用稳定唯一 ID,不要用 position。
-
areContentsTheSame 总返回 true/false
➜ 返回 true 就不会触发绑定;要么 data class 自动 equals,要么手写字段比对。
-
payload 生成了但 onBind(payloads) 里仍整行重绑
➜ 在 payload 分支只更新变化控件(否则失去"局部刷新"的意义)。
-
动画闪烁
➜ 关掉 supportsChangeAnimations 或完善 payload;配合稳定 ID。
-
大列表 diff 占用 CPU
➜ 让 areContentsTheSame 高效;必要时自定义 AsyncDifferConfig 的后台线程池;或分页(Paging 3)。
一页模板(复制即用)
kotlin
class FeedAdapter :
ListAdapter<Row, RecyclerView.ViewHolder>(RowDiff) {
init { setHasStableIds(true) }
override fun getItemId(pos: Int) = stableIdOf(getItem(pos)) // 业务唯一
override fun getItemViewType(pos: Int) = when (getItem(pos)) {
is Row.Banner -> 1
is Row.Article -> 2
is Row.Footer -> 3
}
override fun onCreateViewHolder(p: ViewGroup, vt: Int) = when (vt) {
1 -> BannerVH(...)
2 -> ArticleVH(...)
3 -> FooterVH(...)
else -> error("unknown")
}
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int) =
onBindViewHolder(h, pos, emptyList())
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int, payloads: MutableList<Any>) {
val item = getItem(pos)
if (payloads.isNotEmpty()) {
when (val p = payloads.last()) {
is ArticlePayload -> (h as ArticleVH).bindPartial(p)
// ...
}
return
}
when (h) {
is BannerVH -> h.bind(item as Row.Banner)
is ArticleVH -> h.bind(item as Row.Article)
is FooterVH -> h.bind(item as Row.Footer)
}
}
}
object RowDiff : DiffUtil.ItemCallback<Row>() {
override fun areItemsTheSame(o: Row, n: Row) = o.id == n.id && o::class == n::class
override fun areContentsTheSame(o: Row, n: Row) = o == n
override fun getChangePayload(o: Row, n: Row) = when {
o is Row.Article && n is Row.Article -> ArticlePayload(
title = n.title.takeIf { it != o.title },
liked = n.liked.takeIf { it != o.liked }
).takeIf { it.hasAny() }
else -> null
}
}
一句话收束
用 ListAdapter(含 AsyncListDiffer)+ ItemCallback 让更新"交给差分":只改变的地方被刷新 ,动画自然 ,UI 线程更轻 。再配 稳定 ID 与 payload 局部绑定 ,RecyclerView 就既顺滑 又不闪 ,而你的代码也更工程化。