7、DataBinding与RecyclerView:Item布局绑定、BR与变量ID、Item点击事件处理、DiffUtil与DataBinding结合

RecyclerView 是 Android 开发中最常用的列表组件。DataBinding 和它结合,能省掉大量样板代码。今天我们就来聊聊这个话题。

说实话,我早期做列表页时,每个 ViewHolder 里都要写一堆 findViewById,后来用了 DataBinding,代码量直接砍半。但真正让我觉得「真香」的,是 DataBinding 配合 DiffUtil 做局部刷新——那体验,谁用谁知道。

7.1 Item 布局绑定:从 ViewHolder 到 DataBinding

先看一个最基础的例子。假设我们有一个用户列表,每个 Item 显示用户名和头像。

传统写法是这样的:

// 传统 ViewHolder
class UserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val nameTv: TextView = itemView.findViewById(R.id.nameTv)
    private val avatarIv: ImageView = itemView.findViewById(R.id.avatarIv)

    fun bind(user: User) {
        nameTv.text = user.name
        // 加载头像...
    }
}

用 DataBinding 之后:

// DataBinding ViewHolder
class UserViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(user: User) {
        binding.user = user
        binding.executePendingBindings()
    }
}

你看,ViewHolder 里不再需要手动设置 TextView 了。直接在布局里声明变量,DataBinding 自动帮你填充。

Item 布局文件长这样:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="user" type="com.example.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />

        <ImageView
            android:layout_width="48dp"
            android:layout_height="48dp"
            app:imageUrl="@{user.avatar}" />

    </LinearLayout>
</layout>

Adapter 里怎么用?也很简单:

class UserAdapter : RecyclerView.Adapter<UserViewHolder>() {
    private var users: List<User> = emptyList()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val binding = ItemUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return UserViewHolder(binding)
    }

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

    override fun getItemCount() = users.size

    fun submitList(list: List<User>) {
        users = list
        notifyDataSetChanged()
    }
}
小提示:记得调用 executePendingBindings()。如果不调,DataBinding 可能会延迟到下一帧才更新 UI。在 RecyclerView 这种高频刷新的场景下,延迟会导致闪烁。

7.2 BR 与变量 ID:DataBinding 的「暗号」

用过 DataBinding 的朋友应该见过 BR 这个类。它是什么?说白了,就是 DataBinding 自动生成的变量 ID 映射表。

当你在布局里声明了一个 <variable name="user" type="..." />,DataBinding 就会在 BR 类里生成一个 public static final int user = 0; 的常量。这个 ID 用来标识哪个变量发生了变化。

为什么要这个 ID?因为 DataBinding 需要知道「哪个变量变了,需要重新绑定」。比如你用了 ObservableField 或者 LiveData,数据变化时,DataBinding 通过 BR 里的 ID 找到对应的绑定表达式,只更新受影响的部分。

我遇到过一个问题:自定义了一个 BindingAdapter,但死活不生效。查了半天,发现是 BR 里没有生成对应的 ID。原因是我在布局里用了 import 但没声明 variable。嗯,这个坑我踩过一次就记住了。

核心要点:BR 是 DataBinding 的「索引表」。每个变量对应一个 ID,用于高效的数据更新通知。

7.3 Item 点击事件处理:不止一种方式

Item 点击事件,在 DataBinding 里有三种常见写法。我按推荐程度排序:

方式一:直接在布局里绑定回调(推荐)

<data>
    <variable name="user" type="com.example.User" />
    <variable name="listener" type="com.example.OnItemClickListener" />
</data>

<LinearLayout
    android:onClick="@{() -> listener.onItemClick(user)}"
    ...>
    ...
</LinearLayout>

Adapter 里传入 listener:

class UserAdapter(private val listener: OnItemClickListener) : RecyclerView.Adapter<UserViewHolder>() {
    // ...
}

interface OnItemClickListener {
    fun onItemClick(user: User)
}

这种方式最干净。布局里直接写点击逻辑,Adapter 不需要关心具体行为。

方式二:在 ViewHolder 里设置点击

class UserViewHolder(private val binding: ItemUserBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(user: User, listener: OnItemClickListener) {
        binding.user = user
        binding.root.setOnClickListener { listener.onItemClick(user) }
    }
}

这种方式也行,但 ViewHolder 里多了一行设置点击的代码。我个人觉得不如方式一简洁。

方式三:使用 BindingAdapter

@BindingAdapter("onItemClick")
fun View.setOnItemClick(listener: View.OnClickListener) {
    setOnClickListener(listener)
}

然后在布局里:

<LinearLayout
    app:onItemClick="@{listener}"
    ...>

这种方式适合需要复用的点击逻辑。但说实话,日常开发中用得不多。

注意:在布局里写 Lambda 表达式时,注意不要造成内存泄漏。如果 listener 持有 Activity 引用,而 Activity 被销毁后 Adapter 还在,就会泄漏。建议使用弱引用或者确保在 onDetachedFromRecyclerView 时清理。

7.4 DiffUtil 与 DataBinding 结合:局部刷新才是王道

为什么需要 DiffUtil?你想想看,如果列表有 1000 条数据,你只改了第 500 条的用户名。用 notifyDataSetChanged() 会刷新全部,界面会闪一下,性能也差。

DiffUtil 能算出「哪些 Item 变了、哪些新增了、哪些删除了」,然后只更新变化的部分。

和 DataBinding 结合后,效果更棒。因为 DataBinding 本身就能感知数据变化,DiffUtil 负责告诉 RecyclerView「哪个位置变了」,DataBinding 负责「把新数据填进去」。

看代码:

class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
        // 判断是否是同一个 Item,通常用 ID
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
        // 判断内容是否相同
        return oldItem == newItem
    }
}

Adapter 里用 AsyncListDiffer

class UserAdapter(private val listener: OnItemClickListener) : RecyclerView.Adapter<UserViewHolder>() {
    private val differ = AsyncListDiffer(this, UserDiffCallback())

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

    override fun getItemCount() = differ.currentList.size

    fun submitList(list: List<User>) {
        differ.submitList(list)
    }
}

注意,这里 submitList 是异步的。DiffUtil 会在后台线程计算差异,然后自动在主线程更新。你不需要手动调用 notifyItemChanged 之类的方法。

我曾经在一个聊天列表里用过这个组合。消息列表每秒都可能新增或更新,如果用 notifyDataSetChanged,界面会一直闪。换成 DiffUtil + DataBinding 后,只有真正变化的消息才会刷新,流畅度提升很明显。

最佳实践:DiffUtil + DataBinding + RecyclerView 是黄金组合。DiffUtil 负责「精准定位变化」,DataBinding 负责「高效更新 UI」,RecyclerView 负责「流畅滚动」。三者各司其职。

7.5 避坑指南

最后分享几个我踩过的坑:

  • BR 不生成?检查布局文件里是否声明了 <variable>。如果只用了 import 没声明变量,BR 不会生成对应 ID。
  • 点击事件不触发?检查布局里 android:onClick 的写法。Lambda 表达式要写对,比如 @{() -> listener.onItemClick(user)},括号不能少。
  • DiffUtil 不生效?检查 areItemsTheSameareContentsTheSame 的逻辑。前者用 ID 判断,后者用 equals 判断。如果两个方法都返回 false,DiffUtil 会认为所有 Item 都变了,等于没优化。
  • 列表闪烁?检查是否调用了 executePendingBindings()。另外,如果 Item 布局里有动画或者过渡,也可能导致闪烁。

嗯,以上就是 DataBinding 与 RecyclerView 结合的核心内容。从 Item 布局绑定,到 BR 变量 ID,再到点击事件和 DiffUtil,每一步都有它的设计意图。理解了这些,你就能写出更流畅、更易维护的列表页面。

DataBinding + RecyclerView 知识体系 RecyclerView Item布局绑定 BR与变量ID 点击事件处理 DiffUtil结合 ViewHolder + DataBinding 布局中声明 variable executePendingBindings() 自动生成 BR 类 变量 ID 映射 高效数据更新通知 布局中绑定回调 ViewHolder 设置点击 BindingAdapter 方式 DiffUtil.ItemCallback AsyncListDiffer 局部刷新 + 流畅滚动
公众号:蓝海资料掘金营,微信 deep3321