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。嗯,这个坑我踩过一次就记住了。
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}"
...>
这种方式适合需要复用的点击逻辑。但说实话,日常开发中用得不多。
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 后,只有真正变化的消息才会刷新,流畅度提升很明显。
7.5 避坑指南
最后分享几个我踩过的坑:
- BR 不生成?检查布局文件里是否声明了
<variable>。如果只用了import没声明变量,BR 不会生成对应 ID。 - 点击事件不触发?检查布局里
android:onClick的写法。Lambda 表达式要写对,比如@{() -> listener.onItemClick(user)},括号不能少。 - DiffUtil 不生效?检查
areItemsTheSame和areContentsTheSame的逻辑。前者用 ID 判断,后者用 equals 判断。如果两个方法都返回 false,DiffUtil 会认为所有 Item 都变了,等于没优化。 - 列表闪烁?检查是否调用了
executePendingBindings()。另外,如果 Item 布局里有动画或者过渡,也可能导致闪烁。
嗯,以上就是 DataBinding 与 RecyclerView 结合的核心内容。从 Item 布局绑定,到 BR 变量 ID,再到点击事件和 DiffUtil,每一步都有它的设计意图。理解了这些,你就能写出更流畅、更易维护的列表页面。