Android 列表差量更新与 DiffUtil

举报
yd_290101916 发表于 2026/10/16 09:31:56 2026/10/16
【摘要】 Android 列表差量更新与 DiffUtil列表更新最直接的做法是 notifyDataSetChanged,但全量刷新会丢失输入焦点、动画和滚动状态,也会让所有可见 Item 重新绑定。DiffUtil 通过对比新旧数据计算最小变化集,让 RecyclerView 只更新真正变化的项。 一、全量刷新的问题notifyDataSetChanged 让 RecyclerView 忘记当前...

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。目标是减少重绑、保留状态和动画,让列表刷新对用户透明。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。