适配器与 DiffUtil
使用 ListAdapter 和 DiffUtil.ItemCallback 构建高效、带动画的列表适配器。使用 submitList() 实现流畅的局部更新。
适配器与 DiffUtil 是 CoddyKit 上的免费 Android Academy 课时。 这是第 6 节课,共 6 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Android Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Android Academy 课程共包含 6 节课。
notifyDataSetChanged() 的问题
当 RecyclerView 中的数据发生变化时,调用 notifyDataSetChanged() 会重绘每一个项目,即使它们没有变化。这会导致:
- 没有项目动画
- 大型列表的性能较差
- 界面闪烁
DiffUtil 会计算出需要进行的最少变更,并使用平滑动画应用这些变更。
DiffUtil.ItemCallback
定义两个项目的比较方式。需要回答两个问题:
areItemsTheSame— 它们是否代表同一个逻辑项目?(通常比较 ID)areContentsTheSame— 所有可见字段是否完全相同?
data class Post(val id: Int, val title: String, val likes: Int)
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem.id == newItem.id // same post?
}
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
return oldItem == newItem // data class equality (all fields)
}
}ListAdapter
ListAdapter 是内置 DiffUtil 的 RecyclerView.Adapter。将您的 ItemCallback 传给其构造函数,然后调用 submitList() 更新数据:
class PostAdapter : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
inner class ViewHolder(val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position) // use getItem(), not a manual list
holder.binding.tvTitle.text = post.title
holder.binding.tvLikes.text = "${post.likes} likes"
}
}提交新列表
每当数据发生变化时,都要调用 submitList()。ListAdapter 会在后台线程上比较新旧列表,只应用必要的变更:
// In Activity or Fragment:
val adapter = PostAdapter()
binding.recyclerView.adapter = adapter
// Observe ViewModel LiveData:
viewModel.posts.observe(viewLifecycleOwner) { posts ->
adapter.submitList(posts) // diff calculated off main thread
}
// Important: always submit a NEW list, not a mutated old one
// WRONG: currentList.add(newPost); adapter.submitList(currentList)
// RIGHT: adapter.submitList(currentList + newPost)ListAdapter 中的点击监听器
通过构造函数传入点击处理程序,这是常见且简洁的模式:
class PostAdapter(
private val onLike: (Post) -> Unit,
private val onOpen: (Post) -> Unit
) : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position)
holder.binding.tvTitle.text = post.title
holder.binding.btnLike.setOnClickListener { onLike(post) }
holder.itemView.setOnClickListener { onOpen(post) }
}
}
// Usage:
val adapter = PostAdapter(
onLike = { viewModel.likePost(it) },
onOpen = { navigateToDetail(it.id) }
)getChangePayload() — 局部更新
当只有一个字段发生变化时(例如点赞数),可以使用 getChangePayload() 避免完整重新绑定:
class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
override fun areItemsTheSame(oldItem: Post, newItem: Post) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Post, newItem: Post) = oldItem == newItem
override fun getChangePayload(oldItem: Post, newItem: Post): Any? {
// Return non-null payload to trigger partial bind
return if (oldItem.likes != newItem.likes) "likes_changed" else null
}
}
// In Adapter.onBindViewHolder with payloads:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any>) {
if (payloads.contains("likes_changed")) {
holder.binding.tvLikes.text = "${getItem(position).likes} likes"
} else {
super.onBindViewHolder(holder, position, payloads)
}
}多种视图类型
使用 getItemViewType() 在同一个列表中呈现不同的项目布局:
class FeedAdapter : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {
companion object {
const val TYPE_POST = 0
const val TYPE_AD = 1
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is FeedItem.Post -> TYPE_POST
is FeedItem.Ad -> TYPE_AD
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
TYPE_POST -> PostViewHolder(ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
else -> AdViewHolder(ItemAdBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
}ConcatAdapter — 组合列表
ConcatAdapter 会将多个适配器组合成一个 RecyclerView,非常适合用于页眉、页脚和加载状态:
// Separate adapters for different sections
val headerAdapter = HeaderAdapter()
val postAdapter = PostAdapter()
val loaderAdapter = LoadingAdapter()
// Combine them in order
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, postAdapter, loaderAdapter)
// Update each independently
postAdapter.submitList(posts)
loaderAdapter.showLoading(true)AsyncListDiffer — 手动控制
如果无法继承 ListAdapter,可以直接使用 AsyncListDiffer:
class CustomAdapter : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
private val differ = AsyncListDiffer(this, PostDiffCallback())
fun submitList(list: List<Post>) = differ.submitList(list)
fun getItem(position: Int): Post = differ.currentList[position]
override fun getItemCount() = differ.currentList.size
}性能提示
请保持 RecyclerView 的运行速度:
- 如果项目具有唯一且稳定的 ID,请使用
setHasStableIds(true) - 避免在
onBindViewHolder中创建对象 - 使用
RecycledViewPool在多个 RecyclerView 之间共享回收的视图 - 使用
setItemViewCacheSize()增大屏幕外缓存
动画自动获得
ListAdapter 与 DiffUtil 的集成会自动为以下情况触发 DefaultItemAnimator:
- 新项目淡入
- 已删除项目淡出
- 项目位置变化时播放移动动画
设置自定义动画器:recyclerView.itemAnimator = MyAnimator()。设置为 null 可禁用所有动画。
快速检查
您必须在 DiffUtil.ItemCallback 中实现哪两个方法?
总结:适配器与 DiffUtil
用最少的代码构建高效且带动画的列表:
DiffUtil.ItemCallback— 定义身份(areItemsTheSame)和相等性(areContentsTheSame)ListAdapter— 内置异步差异计算;调用submitList()- 始终提交新的列表引用,绝不要修改当前列表
- 使用
getChangePayload()进行局部重新绑定 - 使用
ConcatAdapter组合多个适配器
下一步:使用 Navigation Component 实现多屏幕导航。
常见问题解答
「适配器与 DiffUtil」课时是免费的吗?
是的 — 「适配器与 DiffUtil」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Android Academy 课程的其余内容,请升级到 CoddyKit PRO。 Android Academy 课程共包含 6 节课。
「适配器与 DiffUtil」这节课中我会学到什么?
使用 ListAdapter 和 DiffUtil.ItemCallback 构建高效、带动画的列表适配器。使用 submitList() 实现流畅的局部更新。 你通过在浏览器中直接运行的动手代码来练习 Android Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Android Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Android Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 6 节课,共 6 节。
「适配器与 DiffUtil」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Android Academy 课中编写并运行代码吗?
能。每节 Android Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。