重学Android Jetpack(五)之—DataBinding应用

1,933 阅读21分钟

前言

DataBinding我是没有用过的,谷歌推出它是应该是很早的事情了,主要是配合MVVM这种构建模式使用的一个工具。2019年第一次使用MVVM架构模式开发应用时也在犹豫要不要用DataBinding,但最后还是没有用。主要的原因有几点,一是当时的同事不怎么推荐,觉得在xml写太多代码比较凌乱,又不方便调试(在细看DataBinding后知道是可以调试的,但调试相对以往的操作来说也的确相对麻烦一点),另外大家也不熟,所以就这样不了了之。对于身边一些开发的朋友,其实也没听说过谁在用DataBinding,也没有听过谁推荐。网上查了一下,倒是有很多论证DataBinding好处的文章,也对一些质疑进行回击。小弟认为DataBinding作为官方推出了的一个这么久且还没放弃的组件,它一定是有的优势,借此重温Jetpack的机会必须要对它有一次全面的了解。

简介

官方的描述

  • 数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。(developer.android.com/topic/libra…)

DataBinding可以实现数据的双向绑定,它是MVVM构建模式的一种实现,但非必须,可降低布局和逻辑的耦合性。DataBinding的优势主要有:

  • 双向数据绑定:当数据内容有变化的时候,DataBinding会自动更新UI,不再需要人工通过View的id进行绑定,同时当UI的数据发生改变是,model中的数据同样会同步发生改变。
  • 精简了代码量:当使用DataBinding后,findViewByIdsetOnClickListener()这种代码就不用再写了,Activity或者Fragment会简洁不少;
  • 绑定数据空安全:在xml实现绑定数据的操作是空安全的,空安全就是它会自己判断空指针,所以大大减少了数据绑定带来的空指针问题。

DataBinding的基本使用

我们通过一个模拟加载数据的小案例来捋一下DataBinding的使用流程,后面再说一些使用细节诸如在xml中语法就清晰得多了。

gradle文件修改

在App层级的build.gradle中android节点下添加以下代码就可以使用DataBinding了:

android {
    dataBinding {
        enabled = true
    }	
}

设置布局

声明一个数据model:

@NoArgAnn
data class UserModel(
    var name: String,
    var sex: String
)

这里的注解@NoArgAnn是自定义的,主要是为了data class可以通过无参构造方法获得对象,使用了NoArg和AllOpen插件,如果没有使用过可以看看我的这篇文章:juejin.cn/post/706832…

布局:

要使用DataBinding,布局中的根标记必要要以layout开头,然后包含data和普通布局的layout。看下面MainActivity布局:

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

    <data>
        <!--指定可在此布局使用的变量model 变量名字不可以有下划线,否则系统会找不到文件-->
        <variable
            name="userModel"
            type="com.qisan.databinding.UserModel" />

        <!--OnViewClickHandler 主要处理view点击事件回调后的处理-->
        <variable
            name="onViewClickHandler"
            type="com.qisan.databinding.MainActivity.OnViewClickHandler" />

        <!--声明一个基本数据类型变量 这里更多是我们平时在activity中的使用的全局标志位之类-->
        <variable
            name="isShow"
            type="Boolean" />

        <!--把View倒入进来,xml才知道-->
        <import type="android.view.View" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center">

        <!--include进来的布局也可以使用dataBinding-->
        <include
            app:isShow="@{isShow}"
            layout="@layout/layout_loading" />

        <!--布局中的表达式使用“@{}”语法给控件赋值。-->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@{userModel.name}"
            android:textColor="@color/black"
            android:textSize="16sp"
            android:textStyle="bold"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="4dp"
            android:text="@{userModel.sex}"
            android:textColor="@color/teal_700"
            android:textSize="14sp"
            android:textStyle="bold"/>

        <!--() -> onViewClickHandler.userChangeClick()是指定点击之后执行的方法 如果你需要view的话也可这样:
            (view) -> onViewClickHandler.userChangeClick(view)-->
        <!--显示隐藏的做法:@{isShow ? View.GONE : View.VISIBLE}-->
        <Button
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_margin="16dp"
            android:background="@color/teal_700"
            android:gravity="center"
            android:onClick="@{() -> onViewClickHandler.userChangeClick()}"
            android:text="加载数据"
            android:textColor="@color/white"
            android:textSize="16sp"
            tools:ignore="HardcodedText"
            android:visibility="@{isShow ? View.GONE : View.VISIBLE}"/>
    </LinearLayout>

</layout>

这里要说一下include进来布局layout_loading.xml代码:

<?xml version="1.0" encoding="utf-8"?>
<layout>

    <data>
        <variable
            name="isShow"
            type="Boolean" />
        <import type="android.view.View" />
    </data>

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/rl_loading"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="@{isShow ? View.VISIBLE : View.GONE,default=gone}">

        <ProgressBar
            android:id="@+id/progressBar2"
            style="?android:attr/progressBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_centerVertical="true" />
    </RelativeLayout>
</layout>

这里一定要把data内容声明一遍,不然isShow这个值无法达到双向绑定的目的,而且isShow在根布局那里通过app:isShow="@{isShow}"来传递值。

Activity的处理:

Activity中的处理主要是通过DataBindingUtil绑定布局,并绑定在xml声明的data。这里要注意的是,在xml布局设置好了之后记得make下项目,不然没有生成布局文件对应的绑定类。这里把MianActivity的代码贴出来:

class MainActivity : AppCompatActivity() {

    private lateinit var userModel: UserModel
    private lateinit var mainBinding: ActivityMainBinding
    private var count: Int = 0
    protected var isShow = false
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        //通过反射获取无参构造方法获得一个对象
        userModel = UserModel::class.java.newInstance()

        mainBinding.onViewClickHandler = OnViewClickHandler()
    }

    inner class OnViewClickHandler {
        fun userChangeClick() {
            isShow = true
            mainBinding.isShow = isShow
            //模拟一下数据加载
            Handler(Looper.getMainLooper()).postDelayed({
                when (count) {
                    0 -> {
                        userModel.name = "小明"
                        userModel.sex = "男"
                    }
                    1 -> {
                        userModel.name = "李梅梅"
                        userModel.sex = "女"
                    }
                    else -> {
                        userModel.name = "张三"
                        userModel.sex = "男"
                    }
                }
                //数据绑定
                mainBinding.userModel = userModel
                isShow = false
                mainBinding.isShow = isShow
                if(count == 2) count=0 else count++
            }, 2000)
        }
    }
}

这里把点击事件回调处理的OnViewClickHandler类写成了一个内部类,这样是为了方便使用全局变量作为演示,一般开发中我们会单独作为一个类来处理,这样代码分离也会更好一点,这点在MVVM架构中会有更好的体现。

案例演示效果:

1001.gif

在使用DataBinding注意一下几点:

  • layout用作布局的根节点,只能包裹一个View标签,且不能包裹merge标签;
  • <data>标签只能存在一个,而且它还有个属性class,我们可以自定义DataBinding生成的类名及路径,但我们一般不需要这样做:
<!--自定义类名-->
<data class="CustomDataBinding">
</data>

<!--自定义生成路径以及类型-->
<data class="com.qisan.databinding.CustomDataBinding">
</data>

DataBinding的绑定表达式

@{}表达式中支持的运算操作符和关键字

在上面的使用案例中,我们看到layout_loading.xml中对加载效果layout隐藏显示是使用表达式来处理的:

android:visibility="@{isShow ? View.VISIBLE : View.GONE,default=gone}"

后面加了一个default=gone可以默认隐藏,如果不加会默认显示出来。此外,xml文件支持以下的运算操作符和关键字的表达式:

  • 算术运算符 + - / * %
  • 字符串连接运算符 +
  • 逻辑运算符 && ||
  • 二元运算符 & | ^
  • 一元运算符 + - ! ~
  • 移位运算符 >> >>> <<
  • 比较运算符 == > < >= <=(请注意,< 需要转义为 &lt;
  • instanceof
  • 分组运算符 ()
  • 字面量运算符 - 字符、字符串、数字、null
  • 类型转换
  • 方法调用
  • 字段访问
  • 数组访问 []
  • 三元运算符 ?:

表达式不支持的关键字和操作:

  • this
  • super
  • new
  • 显式泛型调用

空合并运算符

上面这些操作符我们或多或少都见到过或用过,另外DataBinding还新增了Null合并运算符(??),或者叫空合并运算符,我们看一下它的使用格式:

android:text="@{userModel.name ?? userModel.firstName}"

意思也很简单,就是左边的值不是null,则空合并运算符选择左边的值,如果左边的值为 null,则选择右边的值。

空安全

我们在前面说过DataBinding的数据绑定是空安全的,这是因为它在生产类中检查值是否为ull,并避免出现空指针异常。看下面代码:

android:text="@{userModel.name}"

如果userModel是null,则userModel.name默认返回null,如果是Int类型的值,如userModel.age,如果userModel为null,则默认返回0。

视图引用

当前View(即控件)可以通过其他View的id引用其属性值,通过将id转换成驼峰式写法即可引用,请看下面代码:

<EditText
    android:id="@+id/et_account"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:hint="请输入账号"
    android:textColor="@color/teal_200"
    android:inputType="text"
    android:textColorHint="@color/purple_500"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"/>

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{etAccount.text}"
    android:layout_marginTop="16dp"
    android:textColor="@color/teal_200"
    android:textSize="16sp"
    android:textStyle="bold"/>

这样当输入的值变化时,TextView的值也会跟着变化:

1122.gif

如果要在xml直接通过etAccount.text拿值传给view的监听回调函数会编译不通过,我们先找OnViewClickHandler添加一个方法:

fun getEditTtext(editText: String){
    Toast.makeText(this@MainActivity,editText,Toast.LENGTH_SHORT).show()
}

然后在ButtononClick中调用:

<Button
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:layout_margin="16dp"
    android:background="@color/teal_700"
    android:gravity="center"
    android:onClick="@{() -> onViewClickHandler.getEditTtext(etAccount.text)}"
    android:text="加载数据"
    android:textColor="@color/white"
    android:textSize="16sp"
    android:visibility="@{isShow ? View.GONE : View.VISIBLE}"/>

结果运行编译不通过,改成etAccount.text.toString()也不行,在官方文档也没有找到解决方法,不知道是不是我写法的问题,如果有同学知道的话麻烦在评论区告诉我。这个方法不行,我们可以这样做,先在<data>标签中添加一个变量:

<variable
    name="inputValue"
    type="String" />

然后在EditText中对inputValue赋值:

<EditText
    android:id="@+id/et_account"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:hint="请输入账号"
    android:textColor="@color/teal_200"
    android:inputType="text"
    android:textColorHint="@color/purple_500"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    android:text="@={inputValue}"/>

这样,inputValue的值就是EditText输入的值,Button中的点击操作改成这样:

android:onClick="@{() -> onViewClickHandler.getEditTtext(inputValue)}"

运行效果:

1123.gif

集合

为方便起见,可使用 [] 运算符访问常见集合,例如数组、列表、稀疏列表和映射。

<data>
   <import type="android.util.SparseArray"/>
   <import type="java.util.Map"/>
   <import type="java.util.List"/>
   <variable name="list" type="List&lt;String>"/>
   <variable name="sparse" type="SparseArray&lt;String>"/>
   <variable name="map" type="Map&lt;String, String>"/>
   <variable name="index" type="int"/>
   <variable name="key" type="String"/>
</data>
  
android:text="@{list[index]}"

android:text="@{sparse[index]}"

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

这里要注意的是:要使XML不含语法错误,您必须转义 < 字符。例如:不要写成 List<String> 形式,而是必须写成 List&lt;String>。您还可以使用 object.key 表示法在映射中引用值。例如,以上示例中的 @{map[key]} 可替换为 @{map.key}

字符串字面量

我们可以使用单引号括住特性值,这样就可以在表达式中使用双引号,如以下示例所示:

android:text='@{map["firstName"]}'
    

也可以使用双引号括住特性值。如果这样做,则还应使用反单引号 ` 将字符串字面量括起来:

android:text="@{map[`firstName`]}"
    

资源引用

xml文件中可以通过表达式使用以下语法引用应用资源:

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
    

large只是一个布尔值的判断哈。

我们也可以通过提供参数来评估格式字符串和复数形式:

android:text="@{@string/nameFormat(firstName, lastName)}"

android:text="@{@plurals/banana(bananaCount)}"
    

我们也可以将属性引用视图引用作为资源参数进行传递:

android:text="@{@string/example_resource(user.lastName, exampleText.text)}"
    

某些资源需要显式类型求值,如下图:

1124.png

事件处理

通过数据绑定,我们可以编写从视图分派的表达式处理事件(例如,onClick()方法)。事件特性名称由监听器方法的名称确定,但有一些例外情况。例如,View.OnClickListener有一个onClick()方法,所以该事件的特性为android:onClick。这个从我们文章开头介绍的案例已经知道了,此外还有一些专门针对点击事件的事件处理方法,这些处理方法需要使用除android:onClick以外的特性来避免冲突。您可以使用以下属性来避免这些类型的冲突:

1125.png

我们可以通过可以使用方法引用 或 监听器绑定 来进行事件处理。

方法引用

//定义的方法引用
class MyHandlers {
    fun onClickFriend(view: View) { ... }
}
    
<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="handlers" type="com.example.MyHandlers"/>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"
               android:onClick="@{handlers::onClickFriend}"/>
       </LinearLayout>
    </layout>

方法引用的方式要注意的是:引用的方法,参数需要和监听器方法签名一致 (参数类型、个数),所以onClickFriend方法的参数类型是View.

监听器绑定

class Presenter {
   fun onSaveClick(task: Task){}
}


<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable name="task" type="com.android.example.Task" />
            <variable name="presenter" type="com.android.example.Presenter" />
        </data>
        <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
            <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:onClick="@{() -> presenter.onSaveClick(task)}" />
        </LinearLayout>
</layout>
    

从上面两段模板代码可以看出引用方法需要和监听器的参数一致,而监听器绑定更加灵活,可在运行时动态运行lambda表达式,参数无需一致。在我们文章开头展示的案例中就是采用的监听器绑定实现,区别的就是没有另外再写Task来执行逻辑,而是直接在方法体内实现了。

监听器如果有多个参数并且需要拿到点击事件的参数需要这样写:

class Presenter {
   fun onCompletedChanged(task: Task, completed: Boolean){}
}

//监听CheckBox选中状态变化
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
          android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
   

这里有点要注意:

  • 如果监听事件的返回类型不为Void,lambda表达式也要返回相同类型的值!
  • 监听器表达式这种写法功能强大,可以使代码更易阅读,但不建议写太复杂的表达式,本末倒置,反而使得布局难以阅读和维护。

使用可观察数据对象

可观察性是指一个对象将其数据变化告知其他对象的能力。通过数据绑定库,您可以让对象、字段或集合变为可观察。任何对象都可用于数据绑定,但修改对象不会自动使界面更新。通过数据绑定,数据对象可在其数据发生更改时通知其他对象,即监听器。DataBinding中可观察的数据对象有三种不同类型:字段、集合和对象,通过数据绑定,数据对象可在数据发生更改时通知其他对象,即监听器。

可观察字段

在创建实现Observable接口的类时要完成一些操作,但如果您的类只有少数几个属性,这样操作的意义不大。在这种情况下,您可以使用通用 Observable 类和以下特定于基元的类,将字段设为可观察字段:

  • ObservableBoolean
  • ObservableByte
  • ObservableChar
  • ObservableShort
  • ObservableInt
  • ObservableLong
  • ObservableFloat
  • ObservableDouble
  • ObservableParcelable

可观察字段是具有单个字段的自包含可观察对象。原语版本避免在访问操作期间封箱和开箱。如需使用此机制,请采用 Java编程语言创建 public final 属性,或在Kotlin中创建只读属性,如以下示例所示:

class LoginInfo {
    val firstName = ObservableField<String>()
    val lastName = ObservableField<String>()
    val age = ObservableInt()
}

然后通过get()获取值,set("")来设置值:

loginInfo.firstName.set("")
loginInfo.firstName.get()

在对应的Observable类中的set方法中都调用notifyChange()方法更新值:

public void set(T value) {
    if (value != mValue) {
        mValue = value;
        notifyChange();
    }
}

可观察集合

某些应用使用动态结构来保存数据。可观察集合允许使用键访问这些结构。当键为引用类型(如 String)时,ObservableArrayMap类非常有用,如以下示例所示:

 ObservableArrayMap<String, Any>().apply {
        put("firstName", "Google")
        put("lastName", "Inc.")
        put("age", 17)
    }

布局中通过字符串key找到值:

<data>
        <import type="android.databinding.ObservableMap"/>
        <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
        android:text="@{user.lastName}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="@{String.valueOf(1 + (Integer)user.age)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

当键为整数时,使用ObservableArrayList,如下所示:

    ObservableArrayList<Any>().apply {
        add("Google")
        add("Inc.")
        add(17)
    }

// 布局中通过索引访问列表
<data>
    <import type="android.databinding.ObservableList"/>
    <variable name="user" type="ObservableList<Object>"/>
</data>
<TextView android:text="@{user[index]}" ... />
    

可观察对象

实现Observable接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。

Observable接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable类。实现BaseObservable的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable注释,然后在setter中调用 notifyPropertyChanged()方法,如以下示例所示:

class LoginInfo : BaseObservable() {

    @get:Bindable
    var firstName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.firstName)
        }

    @get:Bindable
    var lastName: String = ""
        set(value) {
            field = value
            notifyPropertyChanged(BR.lastName)
        }
}

数据绑定在模块包中生成一个名为 BR 的类,该类包含用于数据绑定的资源的ID。在编译期间,Bindable注释会在BR类文件中生成一个条目。如果数据类的基类无法更改,Observable接口可以使用PropertyChangeRegistry对象实现,以便有效地注册和通知监听器。

这里我直接编译一直报错,BR文件是已经导进来了的,但说找不到BR.firstName找不到,这里需要在app的build.gradle加入Kotlin-apt插件:

apply plugin: 'kotlin-kapt'

kapt {
    generateStubs = true
}

回到我们文章开头的案例也可以改成这样子,不用在修改数据之后在通过viewDataBinding再set一次,但data class中如何使用实现set方法还不清楚如何解决,如果你有解决方案麻烦评论区告诉我哈。

生成的绑定类

数据绑定库可以生成用于访问布局的变量和视图的绑定类。生成的绑定类将布局变量与布局中的视图关联起来。绑定类的名称和包可以自定义。所有生成的绑定类都是从ViewDataBinding类继承而来的。系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为Pascal大小写形式并在末尾添加 Binding 后缀。以上布局文件名为activity_main.xml,因此生成的对应类为ActivityMainBinding。此类包含从布局属性(例如,user变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值。

创建绑定对象

我们一般通过DataBindingUtil来创建绑定对象,它里面有很多种绑定创建方法,上面的案例是其中的一种。

val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
setContentView(binding.root)

//
val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)

如果布局是使用其他机制扩充的,可单独绑定,如下所示:

val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot) 

有时,系统无法预先知道绑定类型。在这种情况下,可以使用DataBindingUtil类创建绑定,如以下代码段所示:

    val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
    val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)

如果您要在 FragmentListView 或 RecyclerView 适配器中使用数据绑定项,您可能更愿意使用绑定类或DataBindingUtil类的inflate()方法,如以下代码示例所示:

    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

带 ID 的视图

DataBinding会对布局中拥有ID的每个View在绑定类中创建不可变字段。例如,数据绑定库会根据以下布局创建TextView类型的firstNamelastName字段:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"
               android:id="@+id/firstName"/>
               
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"
               android:id="@+id/lastName"/>
               
       </LinearLayout>
    </layout>
    

该库一次性从视图层次结构中提取包含ID的视图。相较于针对布局中的每个视图调用findViewById()方法,这种机制速度更快。如果没有数据绑定,则ID并不是必不可少的,但仍有一些情况必须能够从代码访问视图。

变量

DataBinding会为布局中声明的每个变量生成getter、setter方法。

ViewStub

占位置,惰性加载,当ViewStub被inflate或setVisible可见,它会从视图层次结构消失,如果想绑定里面的View,需要在监听 OnInflateListener,在此完成绑定.

即时绑定

当可变或可观察对象发生更改时,绑定会按照计划在下一帧之前发生更改。如果需要立即执行绑定,强制执行,可 executePendingBindings(),但要注意,此方法必须运行在UI线程

高级绑定(Adapter使用绑定)

有时,系统并不知道特定的绑定类。例如,针对任意布局运行的RecyclerView.Adapter不知道特定绑定类。在调用onBindViewHolder()方法时,仍必须指定绑定值。

在以下示例中,RecyclerView 绑定到的所有布局都有item变量。BindingHolder 象具有一个getBinding()方法,这个方法返回ViewDataBinding基类。

class BindingHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
    lateinit var binding: ViewDataBinding
}

class MyAdapter(data: List<User>) : RecyclerView.Adapter<BindingHolder>() {
    private var mData = data

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
        // 绑定
        val binding: ViewDataBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_layout,
            parent,
            false
        )
        val holder = BindingHolder(binding.root)
        holder.binding = binding
        return holder
    }

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        val user = mData[position]
        holder.binding.setVariable(BR.mUser, user)
    }

    override fun getItemCount() = mData.size
}

绑定适配器

绑定适配器负责发出相应的框架调用来设置值。例如,设置属性值就像调用setText()方法一样。再比如,设置事件监听器就像调用 setOnClickListener() 方法。

数据绑定库允许您通过使用适配器指定为设置值而调用的方法、提供您自己的绑定逻辑,以及指定返回对象的类型。

自动选择方法

属性搜索对应方法,不会考虑命名空间,只考虑属性名称类型 ,如:

android:text="@{user.name}"

如果user.getName()的返回值为String,查找接受String参数的setText()方法,所以表达式返回正确的类型很重要,必要时你还可以根据需要进行类型转换。

指定自定义方法名称

一些属性具有名称不符的 setter 方法。在这些情况下,某个特性可能会使用BindingMethods注释与 setter 相关联。注释与类一起使用,可以包含多个BindingMethod注释,每个注释对应一个重命名的方法。绑定方法是可添加到应用中任何类的注释。在以下示例中,android:tint属性与 setImageTintList(ColorStateList) 方法相关联,而不与setTint()方法相关联:

    @BindingMethods(value = [
        BindingMethod(
            type = android.widget.ImageView::class,
            attribute = "android:tint",
            method = "setImageTintList")])

    

大多数情况下,您无需在 Android 框架类中重命名 setter。特性已使用命名惯例实现,可自动查找匹配的方法。

提供自定义逻辑

一些属性需要自定义绑定逻辑。例如,android:paddingLeft 特性没有关联的setter,而是提供了setPadding(left, top, right, bottom)方法。使用BindingAdapter注释的静态绑定适配器方法支持自定义特性 setter 的调用方式。

Android 框架类的特性已经创建了 BindingAdapter 注释。例如,以下示例展示了 paddingLeft 属性的绑定适配器:

    @BindingAdapter("android:paddingLeft")
    fun setPaddingLeft(view: View, padding: Int) {
        view.setPadding(padding,
                    view.getPaddingTop(),
                    view.getPaddingRight(),
                    view.getPaddingBottom())
    }

参数类型非常重要。第一个参数用于确定与特性关联的视图类型,第二个参数用于确定在给定特性的绑定表达式中接受的类型。

使用接收多个属性的适配器:

    @BindingAdapter("imageUrl", "error")
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }  

在布局中使用适配器,如以下示例所示。请注意,@drawable/venueError引用应用中的资源。使用 @{} 将资源括起来可使其成为有效的绑定表达式:

<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
    

如果ImageView同时使用了imageUrl、error,且前者是String,后者是Drawable,就会调用适配器。

如果我们要设置了任意属性就调用适配器,可以将适配器的 requireAll 设置为 false,示例如下:

@BindingAdapter(value = ["imageUrl""placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}
  • requireAll设置为false,没填写的属性将为null,需要做好非空判断!
  • 上述写法命名空间可以随意,写xxx:imageUrl也是可以的,但如果定义了如android:imageUrl就只能用这个命名空间;
  • 自定义的绑定适配器和默认数据绑定适配器冲突,会使用自定义的绑定适配器;
  • @BindingMethod属性名和@BindingAdapter定义的属性名相同会冲突报错。

对象转换

自动转换对象

当绑定表达式返回 Object 时,库会选择用于设置属性值的方法。Object 会转换为所选方法的参数类型。对于使用ObservableMap类存储数据的应用,这种行为非常便捷,如以下示例所示:

<TextView
       android:text='@{userMap["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />
    

注意:您还可以使用 object.key 表示法引用映射中的值。例如,以上示例中的@{userMap["lastName"]} 可替换为 @{userMap.lastName}

表达式中的 userMap 对象会返回一个值,该值会自动转换为用于设置 android:text 特性值的 setText(CharSequence) 方法中的参数类型。如果参数类型不明确,则必须在表达式中强制转换返回类型。

自定义转换

在某些情况下,需要在特定类型之间进行自定义转换。例如,视图的 android:background 特性需要 Drawable,但指定的color值是整数。以下示例展示了某个属性需要 Drawable,但结果提供了一个整数:

<View
       android:background="@{isError ? @color/red : @color/white}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>

每当需要Drawable且返回整数时,int都应转换为ColorDrawable,可以使用  @BindingConversion 注解静态方法来完成这个转换。

    @BindingConversion
    fun convertColorToDrawable(color: Int) = ColorDrawable(color)

双向数据绑定

使用单向数据绑定时,您可以为特性设置值,并设置对该特性的变化作出反应的监听器:

    <CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@{viewmodel.rememberMe}"
        android:onCheckedChanged="@{viewmodel.rememberMeChanged}" 
    />
    

双向数据绑定为此过程提供了一种快捷方式:

    <CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@={viewmodel.rememberMe}" 
    />
    

@={}表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。

为了对后台数据的变化作出反应,您可以将您的布局变量设置为Observable(通常为BaseObservable)的实现,并使用@Bindable注释。

    class LoginViewModel : BaseObservable {
        // val data = ...

         @Bindable
        fun getRememberMe(): Boolean {
            return data.rememberMe
        }

        fun setRememberMe(value: Boolean) {
            // Avoids infinite loops. 避免死循环
            if (data.rememberMe != value) {
                data.rememberMe = value

                // React to the change. 对变化做出反应
                saveData()

                // Notify observers of a new value. 更新数据
                notifyPropertyChanged(BR.remember_me)
            }
        }
    }
    

使用自定义特性的双向数据绑定

如果您我们希望结合使用双向数据绑定和自定义特性,则需要使用@InverseBindingAdapter 和 @InverseBindingMethod注释。例如,如果要在名为MyView的自定义视图中对 "time"特性启用双向数据绑定,请完成以下步骤:

1、使用 @BindingAdapter,对用来设置初始值并在值更改时进行更新的方法进行注释:

    @BindingAdapter("time") 
    @JvmStatic fun setTime(view: MyView, newValue: Time) {
        // Important to break potential infinite loops.
        if (view.time != newValue) {
            view.time = newValue
        }
    }

2、使用@InverseBindingAdapter对从视图中读取值的方法进行注释:

    @InverseBindingAdapter("time") 
    @JvmStatic fun getTime(view: MyView) : Time {
        return view.getTime()
    }

此时,数据绑定知道在数据发生更改时要执行的操作(调用使用@BindingAdapter注释的方法)以及当 view 视特性发生更改时要调用的内容(调用InverseBindingListener)。但是,它不知道特性何时或如何更改。

为此,我们需要在视图上设置监听器。这可以是与自定义视图相关联的自定义监听器,也可以是通用事件,例如失去焦点或文本更改。将@BindingAdapter注释添加到设置监听器(用来监听属性更改)的方法中:

    @BindingAdapter("app:timeAttrChanged") 
    @JvmStatic fun setListeners(
            view: MyView,
            attrChange: InverseBindingListener
    ) {
        // Set a listener for click, focus, touch, etc.
    }

该监听器包含一个InverseBindingListener参数。您可以使用InverseBindingListener告知数据绑定系统,特性已更改。然后可以开始调用使用 @InverseBindingAdapter注释的方法,依此类推。

转换器

如果绑定到View对象的变量需要设置格式、转换或更改后才能显示,则可以使用Converter对象。

以显示日期的EditText对象为例:

<EditText
        android:id="@+id/birth_date"
        android:text="@={Converter.dateToString(viewmodel.birthDate)}"
    />
    

viewmodel.birthDate 属性包含 Long 类型的值,因此需要使用转换器设置格式。 由于使用了双向表达式,因此还需要使用反向转换器,以告知库如何将用户提供的字符串转换回后备数据类型(在本例中为 Long)。此过程是通过向其中一个转换器添加 @InverseMethod注释并让此注释引用反向转换器来完成的。以下代码段显示了此配置的一个示例:

    object Converter {
        //注解修饰反向转换器。
        @InverseMethod("stringToDate") 
        @JvmStatic fun dateToString(
            view: EditText, oldValue: Long,
            value: Long
        ): String {
            // Converts long to String.
        }

        @JvmStatic fun stringToDate(
            view: EditText, oldValue: String,
            value: String
        ): Long {
            // Converts String to long.
        }
    }

DataBinding的基本使用就先介绍到这里了,如果后面使用上还有什么坑就细细道来。

总结

DataBinding主要是往前端数据绑定的思想靠,它的确可以简化不少代码,最直观的就是在Activity中少了很多findViewById的代码,也可以绑定数据,不用再用每个控件去setter数据。但DataBinding的确又很难用(郭霖大神也说很难用~),首先在布局中做比较多的数据赋值,或者是数据判断处理,这和传统开发会有很大的不一样,调试来说也比传统的调试要麻烦一些,这些肯定会让很多开发者头痛,小弟也是挺头痛。另外,我们还是要对View诸如点击事件的操作方法逻辑,这一部分的代码并没有少的,只可以说做到了比较好的解耦。如果公司的开发人员都对DataBinding比较熟悉,大家约定好实用规则,确实可以带来很大的开发便利。无论怎么说,DataBinding还是非常优秀的库,能用还是得用。