27、DataBinding 与 RecyclerView:使用 DataBinding 简化 ViewHolder

说实话,写 RecyclerView 的 ViewHolder 写多了,真的会烦。

每次都要 findViewById,每次都要手动把数据塞到 TextView、ImageView 里。代码量不小,而且特别容易漏掉某个控件。我在项目里见过一个 ViewHolder 写了 200 行,光绑定数据就占了 80 行。嗯,这种代码维护起来真的很痛苦。

DataBinding 就是来解决这个问题的。它能把 ViewHolder 里的数据绑定工作,直接交给 XML 去处理。说白了,就是让布局文件自己知道该显示什么数据,不用你在 Kotlin 代码里一行一行地 setText。

为什么需要 DataBinding?

你想想看,传统的 ViewHolder 长什么样?

class OldViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    private val title: TextView = itemView.findViewById(R.id.tv_title)
    private val desc: TextView = itemView.findViewById(R.id.tv_desc)
    private val avatar: ImageView = itemView.findViewById(R.id.iv_avatar)

    fun bind(item: ItemData) {
        title.text = item.title
        desc.text = item.desc
        // 加载图片...
    }
}

这还算简单的。要是布局里有七八个控件,那 bind 方法就变成了一堆 setText 的集合。而且每次新增一个字段,你得改 XML、改数据类、改 ViewHolder,三个地方同步修改,漏一个就崩。

我个人习惯是,能用 DataBinding 的地方绝不用手写 findViewById。为什么?因为少写代码就是少犯错。

开启 DataBinding

首先,你得在 app 的 build.gradle 里开启它:

android {
    ...
    buildFeatures {
        dataBinding = true
    }
}

这一步很简单,但别忘了。我刚开始用的时候,经常忘记加这一行,然后编译报错,找了半天才发现是这里没开。

改造布局文件

接下来,把普通的布局文件改成 DataBinding 布局。只需要在根布局外面包一层 标签:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="item"
            type="com.example.app.ItemData" />
    </data>

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

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@{item.title}" />

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

    </LinearLayout>
</layout>

注意看, 标签里声明了一个变量叫 item,类型是 ItemData。然后在 TextView 的 android:text 属性里,直接用 @{item.title} 就把数据绑上去了。是不是很直观?

ViewHolder 的终极简化

布局改好了,ViewHolder 就可以写成这样:

class BindingViewHolder<T : ViewDataBinding>(
    private val binding: T
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(item: ItemData) {
        // 如果是 ItemDataBinding,可以直接用
        if (binding is ItemDataBinding) {
            binding.item = item
            binding.executePendingBindings()
        }
    }
}

等等,这还不够通用。我一般会写一个更通用的版本:

class BindingViewHolder<T : ViewDataBinding>(
    val binding: T
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(binder: T.() -> Unit) {
        binder(binding)
        binding.executePendingBindings()
    }
}

然后在 Adapter 里这样用:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<ItemDataBinding> {
    val binding = ItemDataBinding.inflate(
        LayoutInflater.from(parent.context),
        parent,
        false
    )
    return BindingViewHolder(binding)
}

override fun onBindViewHolder(holder: BindingViewHolder<ItemDataBinding>, position: Int) {
    val item = items[position]
    holder.bind {
        this.item = item
    }
}

看到了吗?ViewHolder 里没有任何 findViewById,没有任何 setText。所有数据绑定都在 XML 里完成了。代码量至少减少一半。

避坑指南:executePendingBindings

这里有个坑,我曾经踩过。如果你在列表快速滑动时,发现某些 item 显示的数据不对,或者闪烁,那多半是因为没有调用 executePendingBindings()。

DataBinding 默认是延迟绑定的,它会在下一个帧刷新时才去更新 UI。但在 RecyclerView 里,因为 item 会被复用,延迟绑定可能导致旧数据一闪而过。所以我在 bind 方法里总是加上 executePendingBindings(),强制立即刷新。

处理点击事件

DataBinding 还能处理点击事件。你可以在 XML 里直接绑定一个回调:

<data>
    <variable
        name="item"
        type="com.example.app.ItemData" />
    <variable
        name="listener"
        type="com.example.app.OnItemClickListener" />
</data>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{item.title}"
    android:onClick="@{() -> listener.onItemClick(item)}" />

然后在 Adapter 里把 listener 传进去:

holder.bind {
    this.item = item
    this.listener = clickListener
}

这样连 setOnClickListener 都不用写了。我个人觉得这种方式比在 ViewHolder 里写接口回调要干净得多。

性能怎么样?

很多人担心 DataBinding 会影响性能。其实不用担心。DataBinding 在编译期就生成了绑定代码,运行时几乎没有反射开销。而且它内部做了很多优化,比如只在数据变化时才更新 UI,不会做无谓的刷新。

我在一个列表有上千条数据的项目里用过 DataBinding,滑动流畅度跟手写 ViewHolder 几乎没有区别。所以放心用。

核心逻辑图

下面这张图展示了 DataBinding 在 RecyclerView 中的工作流程:

DataBinding + RecyclerView 工作流程 数据源 (List<ItemData>) Adapter (onBindViewHolder) BindingViewHolder (binding.item = data) DataBinding XML (@{item.title}) 关键优势 • 无需 findViewById • 无需手动 setText • 编译期生成绑定代码 • 支持事件绑定 • 代码量减少 50%+ • 性能无损耗

总结一下

DataBinding 配合 RecyclerView,核心就三件事:

  • 改布局:包上 标签,声明变量
  • 改 ViewHolder:用 BindingViewHolder 替代手写 ViewHolder
  • 绑定数据:在 onBindViewHolder 里把数据塞给 binding

做完这三步,你的 ViewHolder 代码基本就只剩几行了。而且可读性更好,维护起来也轻松。

我个人建议,新项目直接上 DataBinding,老项目可以逐步迁移。别怕改代码,改一次,后面就爽了。

小技巧:如果你用的是 Kotlin,可以结合扩展函数进一步简化。比如写一个通用的 Adapter 基类,把 DataBinding 的逻辑封装进去,这样每个列表页只需要写数据类和布局文件就够了。
注意:DataBinding 在模块化项目里需要额外配置。如果你有多个 module,记得在每个 module 的 build.gradle 里都开启 dataBinding。不然编译时会报 "cannot find symbol class XXXBinding" 的错误。

公众号:蓝海资料掘金营,微信 deep3321