0Pricing
Android Academy · 课时

适配器与 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. Kotlin 集合
  2. RecyclerView 基础
  3. RecyclerView 点击事件
  4. SharedPreferences
  5. DataStore 偏好设置
  6. 适配器与 DiffUtil
← 返回 Android Academy