DataBinding,让人又爱又恨的一个框架

3,680 阅读14分钟

简介

刚开始学 DataBinding 的时候,并没有太多框架架构的意识,学了发现,这都啥跟啥,为什么要这么麻烦呢?于是就一直没有用上。

后来在我真正领会了它的用法后,结合 MVVM 框架,发现用起来真的省事很多,它被人嫌弃的原因是因为有一定的门槛,而且错误提示不够友好,我在封装了 MVVMArchitecture 框架后,一直在公司内部推 DataBinding,希望大家用起来,但是事与愿违的是,你可以让大家用,但是不能期望大家用的好,这是很无奈的事情。

本文就如何使用 DataBinding ,以及如何在 MVVMArchitecture 框架中使用做一个解释,当你学会后,相信我,你会爱上 DataBinding 的。

DataBinding 简单来说它是一个数据绑定框架,可以帮你把数据(M)和视图(V)绑定起来,当数据改变时,视图自动更改,当视图被改变时,数据也会相应更改。原理简单来说其实就是通过工具帮你把绑定生成对应的代码,而且生命周期安全,不会内存泄露。

可能 AS 会有提示一些奇奇怪怪的错误,如果是可以正常运行的那种错误提示,可无需理会,比如 BR 类报红色,xml 中的红色提示等,不过也不多,别一提示错误就被吓跑了。

1、MVVMArchitecture 框架对 DataBinding 的封装

框架地址:MVVMArchitecture

首先你的 Activity 必须继承自框架的 DataBindingBaseActivity,告知框架对应的 layout 文件和 ViewModel 类,如下:

class MainActivity : DataBindingBaseActivity<ActivityMainBinding, MainViewModel>(
    R.layout.activity_main, BR.viewModel
) {
    override fun initParam() {
    }

    override fun initViewObservable() {
    }

    override fun initData() {
        super.initData()
    }
}

对应的 MainViewModel 如下:

class MainViewModel(app: Application) : BaseViewModel<BaseModel>(app) {
}

对应的 xml 如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>

        <!--
        声明变量,通常有 vm 的界面,都需要声明对应的 vm 变量
        在构造界面时把该变量传给 BaseXXXX
        -->
        <variable
            name="viewModel"
            type="com.imyyq.sample.MainViewModel"
            />
    </data>

    <LinearLayout
        android:id="@+id/linear"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity"
        >
    </LinearLayout>
</layout>

使用 layout 标签包裹后,会自动生成 ActivityMainBinding 类,在 xml 中使用 variable 标签声明了 viewModel 变量后,会自动生成 BR.viewModel,于是,一个 Activity 就搭建完成了

框架会自动让你的 Activity 拥有两个变量:mBinding,mViewModel,对应 R.layout.activity_main 生成的 ActivityMainBinding 和 MainViewModel。并且让 xml 的 viewModel 也是 MainViewModel 的实例变量。

根据 Id,mBinding 会自动生成 linear 变量,无需再 findViewById 了。注意:如果你只是想解决 findViewById,而不想使用 DataBinding,可继承自 ViewBindingBaseActivity,并开启视图绑定即可。

关联如下:

图1

这样你的 xml 就和 ViewModel 类绑定起来了,xml 就是你的界面,在界面中的操作,可以通过 viewModel 变量流向 ViewModel 类,ViewModel 中的数据要显示在界面上,可以通过 LiveData 或 ObservableXxxx

可能你的 VM 不止一个 MainViewModel,那么就需要自己手动增加了,如下:

class MainActivity : DataBindingBaseActivity<ActivityMainBinding, MainViewModel>(
    R.layout.activity_main, BR.viewModel
) {
    // 除了主 vm,还可以有其他的 vm,来自 fragment-ktx 的 viewModels 扩展,可快速一行代码创建实例
    private val mTestViewModel by viewModels<TestViewModel>()

    override fun initData() {
        super.initData()
        // 关联
        mBinding.setVariable(BR.testViewModel, mTestViewModel)
        // 或者是这样:
        // mBinding.testViewModel = mTestViewModel
    }
}

xml 如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="viewModel"
            type="com.imyyq.sample.MainViewModel"
            />

        <variable
            name="testViewModel"
            type="com.imyyq.sample.TestViewModel"
            />
    </data>

    ....
</layout>

上面已经将 V 和 VM 结合起来了,为啥叫 MVVM 框架呢?因为 M 是 Model 数据层,V 是 View 视图层,VM 是 ViewModel 业务逻辑层。

2、没有 DataBinding 前的世界

在没有 DataBinding 前,也许你的代码是这样的,我们举个例子:执行一个耗时操作后将结果显示在界面上。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity"
    >

    <TextView
        android:id="@+id/tv_msg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
</LinearLayout>

因为没有使用 DataBinding,我们就开启 ViewBinding 并继承自 ViewBindingBaseActivity,如何开启 ViewBinding 请自行搜索。

class MainActivity : ViewBindingBaseActivity<ActivityMainBinding, MainViewModel>() {
    override fun initViewObservable() {
        // 监听 LiveData
        mViewModel.mMutableLiveData.observe(this, Observer {
            mBinding.tvMsg.text = it
        })
    }

    override fun initData() {
        super.initData()
        // 开始加载数据
        mViewModel.loadData()
    }

    // 必须复写此方法提供 Binding 实例
    override fun initBinding(inflater: LayoutInflater, container: ViewGroup?) =
        ActivityMainBinding.inflate(inflater)
}


class MainViewModel(app: Application) : BaseViewModel<BaseModel>(app) {

    val mMutableLiveData = MutableLiveData<String>()

    fun loadData() {
        // 显示加载中对话框
        showLoadingDialog()

        // 协程
        launchUI {
            // 模拟耗时操作
            delay(2000)

            // 结果
            mMutableLiveData.value = "我是结果"

            // 隐藏对话框
            dismissLoadingDialog()
        }
    }

}

以上通过监听 LiveData,这样 VM 就可以把结果给到 Activity 显示了。

3、有了 DataBinding 后的世界

先把布局用 layout 标签包裹。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="viewModel"
            type="com.imyyq.sample.MainViewModel"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity"
        >

        <!-- 再指定 view 的属性 text 要绑定哪个变量 -->
        <TextView
            android:id="@+id/tv_msg"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.MMutableLiveData}"
            />
    </LinearLayout>
</layout>

DataBinding 绑定都是类似于 android:text="@{viewModel.MMutableLiveData}" 这样的 @{} 结构。如此一来,text 属性就和 viewModel.mMutableLiveData 变量绑定了,当 mMutableLiveData 发生变化,text 就会自动变化。其中如果变量开头只有一个小写字母,比如 mMutableLiveData,那么 DataBinding 会自动把该字母变成大写,所以在 xml 中使用才会是 viewModel.MMutableLiveData,当如果变量是 mmMutableLiveData 这种两个小写字母以上的,则不会将首字母大写。

class MainActivity : DataBindingBaseActivity<ActivityMainBinding, MainViewModel>(
    R.layout.activity_main, BR.viewModel
) {
    override fun initData() {
        super.initData()
        // 开始加载数据
        mViewModel.loadData()
    }
}

这样就不需要你手动 observe 了,也不需要手动调用 setText 方法,而且还是生命周期安全的。

DataBinding 从 AS 3.1 一开始支持 LiveData, 要响应 LiveData,还需要进行一个步骤,那就是调用:

mBinding.lifecycleOwner = this

这样才可以正确响应 LiveData,框架已经帮你做了这一步了,所以放心的去使用 LiveData 吧

如果不加这一步,就得将 MutableLiveData 换成 ObservableField,一样是可以的,ObservableField 是 LiveData 没出来前使用的,有了 LiveData 就不需要 ObservableXxxx 了,LiveData 更好用,当然,ObservableXxxx 也可以用,并没有过时,看你需求了,可用的类如下:

图4

也许你会说,咦,好像没省掉多少代码啊,就省了 observe,这么一看的确没省掉多少,但是 DataBinding 的功能还不止于此。

4、DataBinding 用法集合

上面我们知道了,在 xml 中定义 view 的 id 后,会在 Binding 类中自动生成对应的变量,规则就是去除下划线,并从第二个字母开始首字母大写。

Binding 类自动生成后,如何实例化呢?MVVMArchitecture 框架帮你实例化了,使用的是内置的 DataBindingUtil 工具类,通过 inflate 或 setContentView 方法,可传入 xml 后生成指定的 Binding 实例,或者和已经实例化的 View 绑定。如下:

图2

或者根据生成的 Binding 类,调用其 inflate 方法也可以生成对应的实例:ActivityMainBinding.inflate(inflater)。

下面我们列举下 DataBinding 的更多用法。

1. 可以导入 import

<data>

    <import type="android.view.View"/>

    <!-- 命名冲突,使用别名 -->
    <import
        alias="MyView"
        type="com.example.imyyq.test.View"
        />
</data>

导入有什么用呢?就可以使用该类的类属性和类方法啦,比如 View.GONE,String.valueOf 等,下面会说到。

2. variable 的数量可以有多个的

<data>

    <import type="java.util.List"/>

    <!-- variable可以是任意数量的 -->
    <!-- 导入的类型,可以作为引用,如果使用到<>之类的,需要转义 -->
    <variable
        name="userList"
        type="List&lt;User&gt;"
        />
    <variable
        name="user"
        type="com.imyyq.databindingtest.User"
        />
</data>

变量肯定可以定义多个啦,泛型的尖括号需要转义。

3. 自定义自动生成的类名称

默认的 Binding 生成类规则是:xml 的名称去掉下划线,再首字母大写,最后尾部加个 Binding,比如 activity_main,自动生成的类就是:ActivityMainBinding,那么如果你想自定义这个名字呢?如下:

<data class="HeiHei">
</data>

于是自动生成的类就变成了 HeiHei,还可以指定包名:cn.xxx.HeiHei,奇奇怪怪的知识又增加了。

4. 支持 include 标签

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <data>
        <variable
            name="viewModel"
            type="com.imyyq.sample.MainViewModel"
            />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity"
        >

        <TextView
            android:id="@+id/tv_msg"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.MMutableLiveData}"
            />

        <!-- 将布局包含进来,并绑定 test 变量 -->
        <include
            layout="@layout/layout_test"
            app:test="@{viewModel.MMutableLiveData}"
            />
    </LinearLayout>
</layout>

其中 layout_test 如下:

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

    <data>

        <variable
            name="test"
            type="String"
            />
    </data>

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

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{test}"
            />

    </LinearLayout>
</layout>

5. 支持 ViewStub 标签

布局如下:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

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

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="inflateViewStub"
            android:text="Inflate the ViewStub"
            />

        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout="@layout/view_stub"
            />
    </LinearLayout>
</layout>

view_stub.xml 如下:

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

    <data>

        <variable
            name="user"
            type="com.xxx.databinding.model.User"
            />
    </data>

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

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

        <TextView
            android:id="@+id/lastName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}"
            />
    </LinearLayout>
</layout>

代码如下:

// 设置 inflate 回调
mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener()
{
    @Override
    public void onInflate(ViewStub stub, View view)
    {
        // 得到 Binding 实例
        ViewStubBinding binding = DataBindingUtil.bind(view);
        User user = new User("liang", "fei");
        binding.setUser(user);
    }
});

// isInflated 是 DataBinding 自动生成的方法,不是 ViewStub 的方法。可用于判断是否已 inflate
if (!mBinding.viewStub.isInflated())
{
    mBinding.viewStub.getViewStub().inflate();
}

这个特别有用,因为可以判断 ViewStub 是否已经 inflate。

6. 支持双向绑定

比如 EditText,绑定了 LiveData,当 EditText 内容改变的时候,LiveData 的内容也会跟着变化。如下:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.editText}"
    />

关键就是 @={},如此一来,我们就可以直接在 ViewModel 中取得 EditText 的内容数据了,因为 ViewModel 是不可以持有 View 实例的,所以如果没有 DataBinding,要获取 EditText 就只能通过 View 传给 ViewModel。有了 DataBinding 就方便多啦。

内置支持双向绑定的属性如下:

图3

当然我们还可以自定义双向绑定的,后面再来说这个。

7. 支持绑定方法

上面我们的所有操作,都是绑定的变量,能绑定方法吗?当然可以。我们通过 onClick 事件来举例。

val onClick = View.OnClickListener {
    Log.i("MainViewModel", "commonLog - onClick: 变量")
}

fun onClick(v: View) {
    Log.i("MainViewModel", "commonLog - onClick: 方法")
}

fun onClick() {
    Log.i("MainViewModel", "commonLog - onClick: 没有参数,或者有参数也行,但是 xml 中也必须给它传对应的值")
}
<!-- 1、绑定变量 -->
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{viewModel.onClick}"
/>

<!-- 2、绑定方法 -->
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{viewModel::onClick}"
/>

<!-- 3、表达式写法 -->
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="@{() -> viewModel.onClick()}"
/>

其中双冒号 :: 绑定方法的签名必须和属性要求的对象方法签名一致。这里即 OnClickListener 的 onClick 方法。同样的,如果是 onLongClick 监听,那就要求你的方法有返回 Bool 值了。

第三种是表达式的写法,我们下面会说,学过 Java8 的都知道箭头 -> 和双冒号 :: 是 Lambda 表达式。

8. 支持表达式

DataBinding 支持部分表达式,还支持 Lambda 表达式,如上的点击事件,但是建议不要过于复杂,因为 DataBinding 不支持单元测试的,报错也不是很智能。支持的运算符和关键字如下:

图2

没有 this、super、new 和显式泛型调用。示例如下:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:transitionName="@{`image_` + id}"

String 是无需 import 的,因为是 java.lang 包下的,但是 View 就需要了,见上面的导入 import 说明。

如果需要内嵌字符串,那么需要用单引号包裹。或者使用 `` 符号包裹,如上。

(1) 双问号,三元运算符的简化

android:text="@{user.displayName ?? user.lastName}"

displayName 不为 null,则使用 displayName,否则使用 lastName。

(2) 访问集合、数组等元素

跟访问数组一样,用 [] 括号访问,比如:

android:text="@{list[index]}"
android:text="@{map[key]}"

如果是 map 类型的,甚至可以这样:map.key,就跟访问属性一样。而且会自动强转,如果你的 value 不是字符类型的,DataBinding 会强转为符合 text 属性的类型。

(3) 访问资源

比如说 strings,dimens,color 等

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

如果是自定义的 View,比如有个 set 方法参数要求是 Drawable,而 xml 中无法配置 Drawable 对象,只能是 int 型的资源地址,那么就需要自定义转换器了,转换器下面说。

9. 避免空指针异常

如下绑定:

android:text="@{String.valueOf(user.age)}"

如果 user 是空的,不会引发空指针异常,DataBinding 会根据变量的类型,自动设置默认值,比如 age 是 int,那么默认值是 0,不仅是字段,方法也一样。

10. 使用其他 View 的值

<TextView
    android:id="@+id/tv_msg"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.mmMutableLiveData}"
    />

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{tvMsg.text}"
    />

tv_msg 会自动生成 tvMsg 字段,因此可以直接调用相关方法。

11. 在 Preview 窗口显示视图的默认值

注意,这里说的预览窗口的视图,而不是真实运行时的视图,我们经常需要在预览窗口预览视图,比如 TextView,有两种方法可以设置。如下:

<TextView
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.text, default=哈哈哈}"
    tools:text="我咧个去"
/>

其中 tools 的优先级较高,同时设置则显示 tools 设置的内容。

12. 在非 UI 线程更改数据

如果绑定的是 LiveData,那么在子线程更新数据,可以使用 postValue。

如果绑定的是 ObservableXxxx 类型的,其 set 方法也可以直接在子线程中调用,DataBinding 支持在子线程中改变数据,也能响应到 UI 上,而且是线程安全的,不过不可以是集合类型的,比如 ObservableArrayMap,是线程不安全的。

因此如果是集合类型的数据,则应该统一在主线程更改数据。

13. 列表绑定

建议采用第三方绑定库:binding-collection-adapter,可以很方便的实现列表绑定。

如果是自己写 Adapter,最关键的地方就在于使用 setVariable 和 executePendingBindings 方法刷新数据,这里就不展开叙述了,需要自行搜索吧。

14. 转换器

举个例子,比如你有个 float 值,需要设置给 TextView 的 text 属性,而 text 要求是字符串,很明显 float 不符合要求,那么就可以定义一个转换器,将 float 转成 String,如下:

package com.imyyq.sample

import androidx.databinding.BindingConversion

@BindingConversion
fun convert(value: Float?) = value?.toString() + "我列个去"

入参是待转换的类型,出参是转换后的类型,即 Float 和 String。

那么就可以在 xml 中使用了。

val mmMutableLiveData2 = MutableLiveData(100.222f)
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.mmMutableLiveData2}"
/>

可能 xml 会报错,这就是 DataBinding 还不是很智能的一个体现,但是已经是可以运行了。以上只是举个例子,总之这个功能是用来转换类型的,会自动匹配入参和出参。

如果你设置如下属性,可能会感到疑惑:

android:background="@{@android:color/black}"

View 的 setBackground(Drawable background) 方法入参明明是个 Drawable 啊,而 @android:color/black 明显是个 int,int 怎么能赋值给 Drawable 呢?那是因为内置的 androidx.databinding.adapters.Converters 类已经定义了如下转换器:

@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

所以不需要你再定义转换器了。

15. 绑定 View 的方法

DataBinding 库给很多常用属性都添加了支持,比如上面我们用到的 android:text,android:onClick 等,遇到不支持的属性呢?有两种解决方法,可以自定义绑定,也可以根据这个 View 的公共方法名作为属性,自定义绑定我们下面再说,这里先说下绑定 View 的公共方法。

举个例子,TextView 在 xml 中是没有 android:selected 属性的,如果你直接设置,会提示你未知属性:

图3

如果你不使用 DataBinding,即没有 @{} 包裹该值:

android:selected="true"

那么运行都没法运行,因为没有找到这个属性。那么为什么用 DataBinding 就可以呢? 因为在 TextView 中,有 setSelected 方法,因此,只要你的 View 中,不管是自定义的 View,还是官方的 View,只要提供 setXxxx 方法,就可以有 xxxx 属性,即去掉 set 后把首字母小写,而且支持继承的方法。

比如 View 中 setAlpha 方法,TextView 继承自 View,那么就可以有 alpha 属性。不过对未定义的属性使用 android: 开头的,即使能用,也会报黄色,如上图,所以我们最好还是用 app: 作为开头,如下:

图4

如果不是 set 开头的方法,普通的公共方法也可以的,比如 EditText 的 extendSelection(int index) 方法就不是 set 开头的,那么直接:app:extendSelection="@{2}" 即可绑定这个方法。

如果你不喜欢默认的方法名称呢?比如你有个自定义 View,有个方法是 test,如下:

@BindingMethods(value = [
    BindingMethod(
        type = CustomEditText::class,
        attribute = "myTest",
        method = "test")])
class CustomEditText(context: Context?, attrs: AttributeSet?) : AppCompatEditText(context, attrs) {
    fun test(index: Int = 0) {
        Log.i("CustomEditText", "commonLog - test: $index")
    }
}

那么即可在使用 app:test 属性,还可以直接用 myTest 属性

<com.imyyq.sample.CustomEditText
    android:id="@+id/et"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="我是一个小蜜蜂"
    myTest="@{10086}"
    app:test="@{10086}"
/>

通过 BindingMethod 注解,可以自定义属性名,并绑定指定方法。当绑定的数据变化时,会自动调用该方法。

16. 自定义绑定

上面我们讲解了如何绑定已有的方法,那么如果要绑定的方法是无参数的呢?比如 EditText 的 requestFocus() 方法,显然不能通过属性绑定,因为 xml 属性都是有值的。

假设你想对属性自定义自己的逻辑呢?比如 TextView 的 setText 方法,我想要在设置内容的时候做一个判断,把内容中所有的空格换成 = 号,当然,实现这个需求的方式有很多种,这里我们只是拿它来举例,我们该怎么做呢?

答案是通过 BindingAdapter 注解。如下:

@BindingAdapter(value = ["changeText"])
fun changeText(view: TextView, text: String) {
    view.text = text.replace(" ", "=")
}

那么 TextView 就自动拥有了 changeText 这个属性了,如下:

<TextView
    android:id="@+id/tv_msg"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    changeText="@{viewModel.mmMutableLiveData.key2 + `嘿  嘿?`}"
/>

文字间的空格就都会被替换成 = 号。

注意:这里没有指定前缀,所以不能是 app:changeText,一定要和定义的一致

还可以有多个属性:

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
}

其中 requireAll 如果为 true,那么必须同时设定以上两个属性,才能绑定成功。为 false 的话,只要有设置 value 定义的其中一个属性,都可以绑定该方法,其他未设置的 value 都是 null。所以如果是 Kotlin 的话,参数得是可空的。

还可以接收旧值,比如:

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, oldPadding: Int, newPadding: Int) {
    if (oldPadding != newPadding) {
        view.setPadding(newPadding,
            view.getPaddingTop(),
            view.getPaddingRight(),
            view.getPaddingBottom())
    }
}
<TextView
    android:id="@+id/tv_msg"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="@{viewModel.mmMutableLiveData3}"
    changeText="@{viewModel.mmMutableLiveData.key2 + `嘿  嘿?`}"
    />

这样当你使用 android:paddingLeft 属性时,每当值改变,都会调用 BindingAdapter 注解的 setPaddingLeft 方法,并且将旧值和新值都传过去。

建议自定义的属性不要使用 android: 开头,也不要定义前缀,直接是属性名即可,这样的话, android: 是原生定义的,app: 是 View 的方法,而无前缀的就是自定义的,三类分开,更好读。

自定义绑定参数要注意的点

如果你是 Kt 开发的,那么要注意 BindingAdapter 注解的方法参数要可空,而且不要使用默认参数,如下:

@BindingAdapter(
    value = ["onClickCommand", "isInterval", "intervalMilliseconds"],
    requireAll = false
)
fun onClickCommand(
    view: View,
    clickCommand: View.OnClickListener,
    isInterval: Boolean = GlobalConfig.Click.gIsClickInterval,
    intervalMilliseconds: Int = GlobalConfig.Click.gClickIntervalMilliseconds
) {
    if (isInterval) {
        view.clickWithTrigger(intervalMilliseconds.toLong(), clickCommand)
    } else {
        view.setOnClickListener(clickCommand)
    }
}

以上 isInterval 和 intervalMilliseconds 不仅不可空,还设置了默认参数。但是 Java 是没有默认参数的,所以使用时如果不设置这两个参数,依然会生成默认值,如下:

图1

这样 Kt 的默认参数就失效了,所以应该要可空,而且不要默认参数。如下:

@BindingAdapter(
    value = ["onClickCommand", "isInterval", "intervalMilliseconds"],
    requireAll = false
)
fun onClickCommand(
    view: View,
    clickCommand: View.OnClickListener?,
    isInterval: Boolean?,
    intervalMilliseconds: Int?
) {
    var interval = isInterval
    // xml中没有配置,那么使用全局的配置
    if (interval == null) {
        interval = GlobalConfig.Click.gIsClickInterval
    }
    // 没有配置时间,使用全局配置
    var milliseconds = intervalMilliseconds
    if (milliseconds == null) {
        milliseconds = GlobalConfig.Click.gClickIntervalMilliseconds
    }
    if (interval) {
        clickCommand?.let { view.clickWithTrigger(milliseconds.toLong(), it) }
    } else {
        view.setOnClickListener(clickCommand)
    }
}

这样就会生成正确的代码:

图2

17. 自定义双向绑定

上面我们已经说了 EditText 的双向绑定,对于常用的内置属性,使用 @={} 即可,当你是自定义 View 时,需要双向绑定,就得自定义了。我们来个简单的例子,你就明白怎么自定义了。

先来个自定义 View:

class CustomView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    var data: String = ""
}

再自定义绑定的属性:

@BindingAdapter("time")
fun setTime(view: CustomView, newValue: String) {
    Log.i("MyBindingConversion", "commonLog - setTime: $newValue")
    if (view.data != newValue) {
        view.data = newValue
    }
}

@InverseBindingAdapter(attribute = "time", event = "timeAttrChanged")
fun getTime(view: CustomView): String {
    Log.i("MyBindingConversion", "commonLog - getTime: ")
    return view.data
}

@BindingAdapter("timeAttrChanged")
fun setListeners(
    view: CustomView,
    attrChange: InverseBindingListener
) {
    Log.i("MyBindingConversion", "commonLog - setListeners: ")
    // 设置 view 改变的监听,看需求可以是点击,滑动,双击,长按什么的。
    view.setOnClickListener {
        view.data = "click"
        attrChange.onChange()
    }
}

以上定义了一个 CustomView,通过 @BindingAdapter 给它定义了 time 属性,也就是属性变化就会调用 setTime,即正向绑定,这个好理解

然后通过 @InverseBindingAdapter 注解,给 time 属性设置了反向绑定,触发反向绑定的方法可通过 event 属性指定,如上指定了 event = timeAttrChanged,对应了第三个方法 setListeners 方法。其实也可以不用显式声明 event,只要在定义的属性 time 后面加上 AttrChanged,即可自动绑定,如上面的 @BindingAdapter("timeAttrChanged")。

使用如下:

val mmMutableLiveData3 = MutableLiveData("start")

mmMutableLiveData3.value = "end"
<com.imyyq.sample.CustomView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    time="@={viewModel.mmMutableLiveData3}"
    />

运行 log 如下:

I: commonLog - setTime: start
I: commonLog - setListeners: 
I: commonLog - setTime: end

I: commonLog - getTime: 
I: commonLog - setTime: click

首先 mmMutableLiveData3 的默认值是 start,所以调用 setTime,传入 start。然后因为是双向绑定,所以会调用事件监听方法 setListeners,然后 mmMutableLiveData3 的值改变为 end,又再次调用了 setTime。这个过程好理解。

然后 setListeners 定义了 view 的监听事件,当我点击了 view 后,会把 data 属性改成 “click”,然后调用 attrChange.onChange(),则又会触发 setTime 方法,并把新的值传过去。同时 mmMutableLiveData3 的值也会跟着被改成了 click。

以上就完成了自定义的数据双向绑定。要注意的是不要造成死循环了,比如选择地址时,常用滚轮组件,就要避免造成双向绑定死循环。

5、结语

以上就是 DataBinding 的全部功能了,有遗漏吗?如果发现遗漏,欢迎补充哈!我尽可能的都列出来了,主要是要知道有相关的功能,如果觉得某个功能本文说的不详尽,完全可以去搜索与之相关的其他内容。

学完以上内容,你确定不来玩玩 DataBinding 和 MVVMArchitecture 框架吗?

个人觉得 DataBinding 虽好,但是也不要被它禁锢了,刚开始不熟悉的时候,可以慢慢来,遇到实在比较复杂的业务,可以使用 observe LiveData 的方式先完成了,再考虑用 DataBinding 去优化,等你熟悉了,就可以一步到位了。

好了,希望本文可以帮你熟悉使用 DataBinding,祝你好运。