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 工作流程 数据源 (ViewModel) submitList() ListAdapter AsyncListDiffer AsyncListDiffer (后台线程) DiffUtil.Callback dispatchUpdatesTo()

什么时候用 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