RecyclerView 进阶:DiffUtil 与列表更新
背景
在使用 RecyclerView 展示列表时,我们经常需要动态更新数据。直接调用 notifyDataSetChanged() 虽然简单粗暴,但会导致整个列表重绘,性能差且没有动画效果。DiffUtil 应运而生,它能智能计算数据变化,实现局部刷新和流畅动画。
核心概念
什么是 DiffUtil?
DiffUtil 是 Android Support Library 提供的工具类,用于计算两个列表之间的差异。它通过对比旧列表和新列表,找出哪些数据被添加、删除、移动或修改,然后通知 Adapter 进行局部刷新。
工作原理
DiffUtil 使用 Eugene W. Myers 的差分算法,时间复杂度为 O(N + D²),其中 N 是列表长度,D 是变化次数。对于大多数场景,性能远优于全量刷新。
代码实战
1. 创建数据模型
kotlin
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
kotlin
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 中使用
kotlin
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 中调用
kotlin
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 判断
kotlin
// ❌ 错误:position 会变,不能用来判断是否是同一条数据
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldPos == newPos
}
**正确做法:**用唯一 ID 判断
kotlin
// ✅ 正确:用业务 ID 判断
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].id == newList[newPos].id
}
坑 2:areContentsTheSame 逻辑不完整
如果只判断部分字段,会导致该更新的时候没更新。确保所有 UI 展示的字段都参与比较。
坑 3:在子线程调用 calculateDiff
DiffUtil.calculateDiff 是同步方法,数据量大时会在当前线程计算。建议在子线程计算,主线程 dispatch:
kotlin
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,让你的列表更新既高效又流畅!