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,核心就三件事:
- 改布局:包上
标签,声明变量 - 改 ViewHolder:用 BindingViewHolder 替代手写 ViewHolder
- 绑定数据:在 onBindViewHolder 里把数据塞给 binding
做完这三步,你的 ViewHolder 代码基本就只剩几行了。而且可读性更好,维护起来也轻松。
我个人建议,新项目直接上 DataBinding,老项目可以逐步迁移。别怕改代码,改一次,后面就爽了。
公众号:蓝海资料掘金营,微信 deep3321