RecyclerView 进阶:DiffUtil 与列表更新

简介: 本文详解RecyclerView进阶技巧——DiffUtil的原理与实战:通过智能计算新旧列表差异,实现局部刷新与流畅动画,避免notifyDataSetChanged全量重绘。涵盖核心概念、代码实现、避坑指南及性能优化要点,助你打造高性能列表。(239字)

RecyclerView 进阶:DiffUtil 与列表更新

背景

在使用 RecyclerView 展示列表时,我们经常需要动态更新数据。直接调用 notifyDataSetChanged() 虽然简单粗暴,但会导致整个列表重绘,性能差且没有动画效果。DiffUtil 应运而生,它能智能计算数据变化,实现局部刷新和流畅动画。

核心概念

什么是 DiffUtil?

DiffUtil 是 Android Support Library 提供的工具类,用于计算两个列表之间的差异。它通过对比旧列表和新列表,找出哪些数据被添加、删除、移动或修改,然后通知 Adapter 进行局部刷新。

工作原理

DiffUtil 使用 Eugene W. Myers 的差分算法,时间复杂度为 O(N + D²),其中 N 是列表长度,D 是变化次数。对于大多数场景,性能远优于全量刷新。

代码实战

1. 创建数据模型

data class User(
    val id: Long,          // 唯一标识,用于判断是否是同一条数据
    val name: String,
    val age: Int
) {
    // 用于判断内容是否发生变化
    fun isSameContent(other: User): Boolean {
        return this.name == other.name && this.age == other.age
    }
}

2. 实现 DiffUtil.Callback

class UserDiffCallback(
    private val oldList: List<User>,
    private val newList: List<User>
) : DiffUtil.Callback() {

    override fun getOldListSize(): Int = oldList.size

    override fun getNewListSize(): Int = newList.size

    // 判断是否是同一条数据(根据 ID)
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    // 判断数据内容是否相同(用于更新动画)
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].isSameContent(newList[newItemPosition])
    }

    // 可选:返回变化的 payload,用于局部刷新
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]

        val changes = mutableListOf<String>()
        if (oldItem.name != newItem.name) changes.add("name")
        if (oldItem.age != newItem.age) changes.add("age")

        return if (changes.isEmpty()) null else changes
    }
}

3. 在 Adapter 中使用

class UserAdapter : RecyclerView.Adapter<UserAdapter.UserViewHolder>() {

    private var userList: List<User> = emptyList()

    fun submitList(newList: List<User>) {
        val diffResult = DiffUtil.calculateDiff(
            UserDiffCallback(userList, newList)
        )
        userList = newList
        diffResult.dispatchUpdatesTo(this)  // 自动调用 notify 方法
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        holder.bind(userList[position])
    }

    // 支持 payload 的局部刷新
    override fun onBindViewHolder(
        holder: UserViewHolder, 
        position: Int, 
        payloads: MutableList<Any>
    ) {
        if (payloads.isEmpty()) {
            onBindViewHolder(holder, position)
        } else {
            // 只刷新变化的字段
            holder.bindPartial(userList[position], payloads[0] as List<String>)
        }
    }

    class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val nameText: TextView = itemView.findViewById(R.id.nameText)
        private val ageText: TextView = itemView.findViewById(R.id.ageText)

        fun bind(user: User) {
            nameText.text = user.name
            ageText.text = "${user.age}岁"
        }

        fun bindPartial(user: User, changes: List<String>) {
            if (changes.contains("name")) {
                nameText.text = user.name
            }
            if (changes.contains("age")) {
                ageText.text = "${user.age}岁"
            }
        }
    }
}

4. 在 Activity/Fragment 中调用

class MainActivity : AppCompatActivity() {

    private lateinit var adapter: UserAdapter
    private var userList: List<User> = emptyList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = UserAdapter()
        recyclerView.adapter = adapter

        // 模拟数据更新
        btnUpdate.setOnClickListener {
            val updatedList = userList.map { 
                if (it.id == 1L) it.copy(age = it.age + 1) else it 
            }
            adapter.submitList(updatedList)  // 自动计算差异并刷新
            userList = updatedList
        }
    }
}

避坑指南

坑 1:忘记实现 areItemsTheSame

错误做法:直接用 position 判断

// ❌ 错误:position 会变,不能用来判断是否是同一条数据
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
    return oldPos == newPos
}

正确做法:用唯一 ID 判断

// ✅ 正确:用业务 ID 判断
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
    return oldList[oldPos].id == newList[newPos].id
}

坑 2:areContentsTheSame 逻辑不完整

如果只判断部分字段,会导致该更新的时候没更新。确保所有 UI 展示的字段都参与比较。

坑 3:在子线程调用 calculateDiff

DiffUtil.calculateDiff 是同步方法,数据量大时会在当前线程计算。建议在子线程计算,主线程 dispatch:

lifecycleScope.launch(Dispatchers.Default) {
    val diffResult = DiffUtil.calculateDiff(callback)
    withContext(Dispatchers.Main) {
        diffResult.dispatchUpdatesTo(adapter)
    }
}

坑 4:列表数据引用未更新

submitList 后必须更新 Adapter 内部的数据引用,否则下次对比时用的还是旧数据。

总结

DiffUtil 是 RecyclerView 性能优化的重要工具。关键点:

  • areItemsTheSame 用唯一 ID 判断是否是同一条数据
  • areContentsTheSame 判断内容是否变化,决定是否触发更新动画
  • getChangePayload 可选,用于更细粒度的局部刷新
  • 大数据量时在子线程计算 diff,主线程 dispatch

掌握 DiffUtil,让你的列表更新既高效又流畅!

相关文章
|
4天前
|
云安全 人工智能 运维
阿里云SecOps Agent,全新安全跨产品执行体验
自然语言驱动 云安全中心/WAF/CFW/ 等多款安全产品联动
1595 2
|
1天前
|
人工智能 定位技术 SEO
我学 GEO 第 15 天:终于知道AI GEO该如何做?
我是暴走的莉莉酱,边旅行边研究AI GEO的数字游民。专注普通人如何提升“AI可见度”——让AI在回答用户问题时准确识别、理解并推荐你。不讲玄学,只做可测、可调、可持续的GEO实践。
348 122
|
4天前
|
机器学习/深度学习 人工智能 调度
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
HappyHorse 1.1 是新一代视频生成大模型,全面升级动态表现力、角色一致性、指令遵循、视觉质感与音画协同能力。支持I2V/T2V/R2V三类生成,适配短剧、电商广告、品牌营销等场景,提供高质、流畅、可控的AI视频生产力。
577 3
🐴 HappyHorse 1.1 现已上线阿里云百炼!快来查收模型使用指南,现在调用享 6 折~
|
14天前
|
缓存 测试技术 API
Qwen 3.7 Plus 与 Max 实测:性价比与多模态能力差异解析(2026)
2026 年 6 月 1 日,阿里悄无声息地发布了 Qwen 3.7 Plus,距 Qwen 3.7 Max 上线刚好 11 天。同样的 1M 上下文,同样的 35 小时自治上限。但价格才是头条:Plus 是 0.40/M输入,Max是 2.50/M——便宜约 6 倍——并且还能看图、看视频。Vision Arena 上 Plus 已经排到 #16。所以这周真正值得讨论的问题不是”要不要为视觉能力买单”,而是”Max 凭什么用 6 倍价格换来 2 个百分点的 benchmark 领先”。
|
15天前
|
JavaScript 定位技术 API
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
CodeGraph 是一款爆火的本地代码智能工具,通过 tree-sitter 解析 AST 构建结构化知识图谱(存于 SQLite),为编程 Agent 提前生成“代码地图”。它显著降低 Agent 在中大型项目中的探索成本——实测工具调用减少71%、Token 降57%、速度提升46%,支持19+语言及主流框架路由识别,完全离线、无需 API Key。
910 11
CodeGraph 爆火:编程 Agent 需要的不是更多上下文,而是一张提前画好的代码地图
|
8天前
|
缓存 人工智能 运维
GLM 5.2自托管全流程实战:硬件选型、vLLM/SGLang部署与成本盈亏测算
2026年智谱发布GLM 5.2超大混合专家模型,区别于以往仅开放API的闭源大模型,该模型权重以MIT开源协议对外发布,企业与开发者可完整下载、本地审计、私有化部署,实现数据不出环境、自定义微调、自主调度推理资源。GLM 5.2拥有753B总参数,原生支持百万级上下文窗口,在代码生成、长文档推理、数学逻辑等多项基准测试中对标国际顶尖商用模型,是首款可完整自托管的前沿代码向大模型。
651 0
|
2天前
|
消息中间件 人工智能 Kafka
AI 时代,实时入湖正在告别 ETL:从 Kafka 到 Iceberg 的架构减法
本文围绕“零 ETL”这一趋势,讨论流数据入湖为什么需要做架构减法,并结合 Kafka × Table Bucket 的实践,分析一种将通用入湖能力前移到消息与表存储链路中的方案,如何在降低复杂度的同时,兼顾实时性、一致性、Schema 演进、CDC 语义与开放生态兼容。
192 121
|
2天前
|
人工智能 监控 前端开发
Electron 监控:让桌面 Agent 监控触手可及
一行代码实现Electron桌面端全景监控,自动还原崩溃现场、预警内存泄漏、全链路追踪、 SSE流式响应与交互埋点,让 AI 助手运行状态清晰可见,助力快速恢复稳定与流畅。
182 125
|
11天前
|
人工智能 自然语言处理 算法
阿里云百炼Qwen 3.7 Plus与Max实测全解:性价比与多模态能力、成本深度对比
2026年,阿里云百炼平台推出的Qwen 3.7系列成为企业与开发者落地AI应用的核心选择,其中Qwen 3.7 Max与Plus作为两大旗舰版本,定位差异显著:Max是纯文本推理旗舰,专注高强度智能体与复杂逻辑任务;Plus则是多模态全能版,在保留强大文本能力的同时,补齐图像、视频理解能力,且价格大幅降低。本文基于2026年最新实测数据,从核心参数、文本能力、多模态能力、智能体表现、性价比与场景选型六大维度,全面解析两款模型的差异,为用户提供精准选型参考。
537 0