5、DataBinding双向绑定:@={}语法、BaseObservable与ObservableField、LiveData与DataBinding结合、双向绑定的注意事项

双向绑定,说白了就是让数据和界面「互相通知」。你改界面,数据跟着变;你改数据,界面也跟着刷新。单向绑定只能数据→界面,双向绑定则是数据⇄界面。嗯,这个特性在表单场景下特别实用。

5.1 @={} 语法:双向绑定的核心写法

单向绑定我们用 @{},双向绑定就多一个等号:@={}。别小看这个等号,它代表的是「可写」的绑定关系。

<!-- 单向绑定:数据变化时更新界面 -->
<EditText
    android:text="@{viewModel.userName}" />

<!-- 双向绑定:数据变化更新界面,界面变化也更新数据 -->
<EditText
    android:text="@={viewModel.userName}" />

我在项目中第一次用双向绑定时,就踩了个坑——忘记加等号,结果用户输入的内容死活存不到 ViewModel 里。排查了半天才发现是 @= 写成了 @。你想想看,这种细节问题确实容易忽略。

关键点@={} 要求绑定的变量必须是「可观察的」,否则编译会报错。

5.2 BaseObservable 与 ObservableField

要让数据变得「可观察」,有两种主流方式:BaseObservableObservableField。我个人习惯用 ObservableField,因为它写起来更简洁。

5.2.1 BaseObservable 方式

class User : BaseObservable() {
    @get:Bindable
    var name: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.name)
        }

    @get:Bindable
    var age: Int = 0
        set(value) {
            field = value
            notifyPropertyChanged(BR.age)
        }
}

这种方式需要手动调用 notifyPropertyChanged,而且每个字段都要写 @get:Bindable 注解。说实话,字段一多就有点烦了。我记得有个项目里定义了十几个字段,光 setter 就写了一百多行……

5.2.2 ObservableField 方式

class User {
    val name = ObservableField<String>("")
    val age = ObservableInt(0)
}

你看,一行代码搞定一个可观察字段。不需要写 setter,不需要手动通知。直接用 user.name.get() 取值,user.name.set("新名字") 赋值。

我的建议:简单场景用 ObservableField,需要复杂逻辑(比如字段变化时触发其他操作)再用 BaseObservable

5.3 LiveData 与 DataBinding 结合

很多团队现在更倾向于用 LiveData 而不是 ObservableField。为什么?因为 LiveData 生命周期感知,不会造成内存泄漏。而且 ViewModel 里本来就在用 LiveData,没必要再引入一套 Observable 体系。

要让 LiveData 支持 DataBinding,需要两步:

  1. 在 build.gradle 中启用 DataBinding
  2. 在布局中绑定 LiveData 时,设置 LifecycleOwner
// ViewModel
class MyViewModel : ViewModel() {
    val userName = MutableLiveData<String>("初始值")
}

// Activity 或 Fragment
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = myViewModel
<!-- 布局文件 -->
<layout>
    <data>
        <variable name="viewModel" type="com.example.MyViewModel" />
    </data>
    
    <EditText
        android:text="@={viewModel.userName}" />
</layout>

这里有个坑:LiveData 配合双向绑定时,必须设置 LifecycleOwner。我曾经在一个 Fragment 里忘了设 binding.lifecycleOwner,结果界面死活不刷新。排查了半天才发现是 LifecycleOwner 没绑定。

注意:LiveData 的双向绑定在 DataBinding 2.3.0+ 才完全支持。如果你用的版本比较老,可能需要升级。

5.4 双向绑定的注意事项

双向绑定虽然方便,但用不好也会出问题。我总结了几条经验:

5.4.1 避免无限循环

双向绑定最怕的就是「你改我,我改你,大家互相改」的死循环。比如 EditText 的文本变化监听器里又去修改文本本身,就会触发无限递归。

// 错误示范:监听器里修改自身
editText.addTextChangedListener {
    viewModel.userName.value = it.toString()  // 触发 LiveData 更新
    // LiveData 更新又会触发界面刷新,界面刷新又触发监听器...
}

DataBinding 内部其实做了防递归处理,但如果你在自定义 BindingAdapter 里不小心,还是可能出问题。

5.4.2 数据类型要匹配

双向绑定时,布局中的属性类型必须和 ViewModel 中的数据类型完全一致。比如 android:text 期望的是 String,你绑一个 Int 就会编译报错。

// 错误
val age = ObservableInt(25)
// 布局中:android:text="@={viewModel.age}"  // 编译报错!text 需要 String

// 正确
val age = ObservableField<String>("25")

5.4.3 自定义 BindingAdapter 的双向绑定

如果你写了自己的 BindingAdapter,想支持双向绑定,需要用 @InverseBindingAdapter@InverseBindingMethod

@BindingAdapter("app:customText")
fun setCustomText(view: TextView, value: String) {
    view.text = value
}

@InverseBindingAdapter(attribute = "app:customText")
fun getCustomText(view: TextView): String {
    return view.text.toString()
}

嗯,这个写法稍微复杂一点。我建议非必要不自定义双向绑定的 BindingAdapter,直接用系统提供的属性就好。

5.4.4 性能考虑

双向绑定比单向绑定多了一层监听和回调,性能开销自然大一些。如果一个界面有几十个输入框都在用双向绑定,可能会影响首屏渲染速度。我的做法是:只在需要用户输入的地方用双向绑定,纯展示用单向绑定

总结一下:双向绑定是 DataBinding 的「杀手锏」功能,但要用对地方。表单、搜索框、设置页面这些场景非常适合。纯展示列表、静态文本就没必要了。

5.5 知识体系图

DataBinding 双向绑定知识体系 双向绑定 @={} @={} 语法 可观察数据源 LiveData 结合 BaseObservable ObservableField / ObservableInt 等 需设置 LifecycleOwner 注意事项:避免循环、类型匹配、性能考量 核心原则:界面 ⇄ 数据 双向同步,但需谨慎使用

这张图把双向绑定的核心脉络梳理清楚了。从 @={} 语法出发,三种数据源各有适用场景,最后落到注意事项上。我个人觉得,掌握这些就足够应对 90% 的双向绑定需求了。


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