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,让你的列表更新既高效又流畅!

相关文章
|
2月前
|
Shell API 开发工具
Claude Code 实战:Agent Skills
面向已用 Claude Code 写代码的开发者,讲清 Skills 三层结构与完整实操路径,帮你把重复工作流封装成可复用、可 Review 的技能包。
Claude Code 实战:Agent Skills
|
存储 PyTorch 算法框架/工具
Tensor to img && imge to tensor (pytorch的tensor转换)
Tensor to img && imge to tensor (pytorch的tensor转换)
|
2月前
|
人工智能 Linux API
DeepCodex零门槛配置:Codex无缝对接DeepSeek大模型完整指南
在AI编程开发场景中,Codex作为经典的代码生成助手,原生模型选择有限,而DeepSeek系列大模型凭借强大的代码理解、生成与推理能力,成为开发者的优质选择。DeepCodex作为轻量桥接工具,能解决两者协议不兼容问题,实现Codex与DeepSeek的无缝对接,让AI编程助手自由切换模型,满足不同开发场景需求。本文从准备工作、DeepCodex部署、模型配置、功能验证到常见问题排查,提供零门槛全流程操作指南,覆盖Windows、macOS、Linux多平台,新手也能快速完成配置。
677 0
|
2月前
|
缓存 人工智能 自然语言处理
阿里云Qwen3.7-Plus限时优惠:8折起,输入1.6元/每百万tokens,输出6.4元/每百万tokens
Qwen3.7-Plus是通义千问面向智能体时代的高性价比多模态基座模型,具备"看、想、写、做、验"端到端能力,支持视觉界面解析、代码生成、办公自动化等场景,已在猎聘、波司登、富滇银行等行业落地。当前限时8折,输入低至1.6元/百万tokens,输出6.4元/百万tokens;叠加全模型通用抵扣计划可低至4.5折,是企业构建AI智能体兼具性能与成本的理想之选。
|
存储 缓存 NoSQL
跟着源码学IM(十一):一套基于Netty的分布式高可用IM详细设计与实现(有源码)
本文将要分享的是如何从零实现一套基于Netty框架的分布式高可用IM系统,它将支持长连接网关管理、单聊、群聊、聊天记录查询、离线消息存储、消息推送、心跳、分布式唯一ID、红包、消息同步等功能,并且还支持集群部署。
14135 1
|
8月前
|
缓存 API 数据安全/隐私保护
淘宝京东拼多多API应用差异大揭秘,商家必看!
本文深度解析淘宝、京东、拼多多三大电商API在接口风格、权限控制、数据推送等方面的差异,涵盖实战代码与成本优化策略,助力商家高效对接平台系统,提升运营效率。
|
缓存 编解码 Android开发
Android内存优化之图片优化
本文主要探讨Android开发中的图片优化问题,包括图片优化的重要性、OOM错误的成因及解决方法、Android支持的图片格式及其特点。同时介绍了图片储存优化的三种方式:尺寸优化、质量压缩和内存重用,并详细讲解了相关的实现方法与属性。此外,还分析了图片加载优化策略,如异步加载、缓存机制、懒加载等,并结合多级缓存流程提升性能。最后对比了几大主流图片加载框架(Universal ImageLoader、Picasso、Glide、Fresco)的特点与适用场景,重点推荐Fresco在处理大图、动图时的优异表现。这些内容为开发者提供了全面的图片优化解决方案。
553 1
|
Android开发 开发者 索引
Android实战经验之如何使用DiffUtil提升RecyclerView的刷新性能
本文介绍如何使用 `DiffUtil` 实现 `RecyclerView` 数据集的高效更新,避免不必要的全局刷新,尤其适用于处理大量数据场景。通过定义 `DiffUtil.Callback`、计算差异并应用到适配器,可以显著提升性能。同时,文章还列举了常见错误及原因,帮助开发者避免陷阱。
1533 9
|
Kubernetes 持续交付 开发工具
ACK One GitOps:ApplicationSet UI简化多集群GitOps应用管理
ACK One GitOps新发布了多集群应用控制台,支持管理Argo CD ApplicationSet,提升大规模应用和集群的多集群GitOps应用分发管理体验。
|
安全 测试技术 API
一图看懂API测试9种方法
一图看懂API测试九种方法:冒烟测试验证基本功能,功能测试确保符合规格,集成测试检查组件协同工作,回归测试防止新变更引入问题,负载测试评估性能稳定性,压力测试挑战极限负载,安全测试发现并修复漏洞,用户界面测试确保UI与API协调,模糊测试提升异常数据处理鲁棒性。