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 就既顺滑不闪 ,而你的代码也更工程化

相关推荐
青莲8434 小时前
RecyclerView 完全指南
android·前端·面试
青莲8434 小时前
Android WebView 混合开发完整指南
android·前端·面试
37手游后端团队7 小时前
gorm回读机制溯源
后端·面试·github
C雨后彩虹7 小时前
竖直四子棋
java·数据结构·算法·华为·面试
CC码码8 小时前
不修改DOM的高亮黑科技,你可能还不知道
前端·javascript·面试
indexsunny9 小时前
互联网大厂Java面试实战:微服务、Spring Boot与Kafka在电商场景中的应用
java·spring boot·微服务·面试·kafka·电商
自燃人~11 小时前
实战都通用的 Watchdog 原理说明
redis·面试
boooooooom11 小时前
手写高质量深拷贝:攻克循环引用、Symbol、WeakMap等核心难点
javascript·面试
小鸡脚来咯11 小时前
Linux 服务器问题排查指南(面试标准回答)
linux·服务器·面试
C雨后彩虹12 小时前
synchronized高频考点模拟面试过程
java·面试·多线程·并发·lock