RecyclerView 的数据驱动更新

为什么用差分(替代手工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 用 IDContentsSame 用 equalsPayload 用差异字段


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 线程更轻 。再配 稳定 IDpayload 局部绑定 ,RecyclerView 就既顺滑不闪 ,而你的代码也更工程化

相关推荐
uhakadotcom4 小时前
coze的AsyncTokenAuth和coze的TokenAuth有哪些使用的差异?
后端·面试·github
Chejdj4 小时前
StateFlow、SharedFlow 和LiveData区别
android·面试
道可到5 小时前
直接可以拿来的面经 | 从JDK 8到JDK 21:一次团队升级的实战经验与价值复盘
java·面试·架构
南北是北北5 小时前
RecyclerView 进阶绑定:多类型 / 局部刷新(payload)/ 稳定 ID
面试
Hilaku5 小时前
为什么我开始减少逛技术社区,而是去读非技术的书?
前端·javascript·面试
南北是北北6 小时前
RecyclerView 的关键角色与各自职责/协同关系
面试
沐怡旸6 小时前
【底层机制】Handler/Looper 实现线程切换的技术细节
android·面试
自由的疯6 小时前
优雅的代码java
java·后端·面试
.NET修仙日记6 小时前
2025年ASP.NETMVC面试题库全解析
面试·职场和发展·c#·asp.net·mvc·面试题·asp.net mvc