Android Jetpack - Data Binding

以声明的方式将 observable 数据绑定到 UI 元素
参考 https://developer.android.com/topic/libraries/data-binding/

Get started

添加 dataBinding 元素到项目中 app moudule 的 build.gradle 文件中:

1
2
3
4
5
6
android {
...
dataBinding {
enabled = true
}
}

Layouts & binding expressions

使用 Databinding 的 layout 文件与正常的略微有些不同,它使用了 layout 作为 root tag ,里面包裹着 data 元素和真正的 view 元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<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}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>

其中data 标签包裹着的变量 “user“ 即为当前 layout 所使用的属性:

1
<variable name="user" type="com.example.User" />

在使用该属性的表达式中,使用 “@{}“ 的语法来表示引用,这里, TextView 的文字被设置为 userfirstName 属性:

1
2
3
4
<TextView 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
ndroid:text="@{user.firstName}" />

表达式应该尽可能的简洁,因为他们不能被单元测试。如果想要简化复杂的表达式,可以使用 Binding adapters。

Data object

先来假设有一个普通的 Java 对象 – User

1
2
3
4
5
6
7
8
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

下面这种对象的数据永远不会变,这些数据通常都会被读取一次之后也不会被更改,对象也可以遵循公约定义一些 get 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}

从数据绑定的角度来看,上面这两种类的写法是等价的,第一种写法的情况下,android:text 中的 @{user.firstName} 表达式会直接读取该类的 firstName 属性,第二种写法则是直接调用 getFirstName() 方法。另外,如果有 firstName() 方法也会被调用。

Binding data

Binding class 根据 layout 文件自动生成,默认情况下,绑定类的名字根据 layout 文件的名字反向生成并追加 Binding 后缀。上节的 layout 文件名字为 activity_main.xml ,对应的转换后的类名为 MainActivityBinding 。绑定类持有着所有的有关布局信息的绑定,并且知道如何根据绑定的表达式赋值。推荐的做法是在 inflafe layout 的同时创建绑定,如下:

1
2
3
4
5
6
7
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}

gradle 插件版本 3.1.3,亲测 activity_main.xml 生成的类名为 ActivityMainBinding ,并没有反转

在运行时,程序在 UI 上显示了 Test 。另外,也可以通过 LayoutInflater 来获取 view ,示例如下:

1
2
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
View view = binding.getRoot();

如果在 FragmentListViewRecyclerView adapter 中使用数据绑定的话,应该使用绑定类的 inflate() 方法,或是 DataBindingUtil ,示例如下:

1
2
3
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

表达式

一般功能

你可以使用下列操作符和关键字:

  • 数学表达式 + - / * %
  • 字符串拼接 +
  • 逻辑操作符 && ||
  • 二进制运算 & | ^
  • 一元运算 + - ? !
  • 位移 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • 分组 ()
  • 字符,字符串,数字,null
  • 类型转换
  • 方法调用
  • 属性访问
  • 数组访问 []
  • 三元运算符号 ?:

示例如下:

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

不支持的操作

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

空合并操作符

空合并操作符 ?? 会在左边表达式为 null 的情况下返回右边表达式的值

1
2
3
<android:text="@{user.displayName ?? user.lastName}"/>
<!-- 等价于 -->
<android:text="@{user.displayName != null ? user.displayName : user.lastName}"/>

属性引用

表达式可以直接引用同名的属性、get 方法和 ObservableField 对象:

1
<android:text="@{user.lastName}"/>

避免空指针异常

生成的绑定类会自动检查 null 并且避免空指针异常,比如说表达式 @{user.name} 中,如果 user 为空, user.name 会被默认分配 null ,如果是 user.age ,年龄为 int 类型,数据绑定会返回默认值 0

集合

常用的集合,诸如 arrayslistssparse listsmaps 都可以用 [] 来访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List&lt;String&gt;"/>
<variable name="sparse" type="SparseArray&lt;String&gt;"/>
<variable name="map" type="Map&lt;String, String&gt;"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
</data>

<android:text="@{list[index]}"/>

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

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

<android:text="@{map.key}"/>

字符串文字

1
2
<android:text='@{map["firstName"]}'/>
<android:text="@{map[`firstName`]}"/>

资源引用

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

有些资源需要更详细的声明,见下表:

类型 通常引用 表达式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
corlor int @color @color
ColorStateList @color @ColorStateList

事件处理

数据绑定允许通过编写表达式来处理 view 分发的事件(例如 onClick())。事件参数的名字由监听回调的方法名决定,当然也有少数例外:

监听回调 setter 参数写法
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

你可以使用两种机制来处理事件:

  • 方法引用:在你的表达式中可以直接调用符合监听函数签名的方法(方法名、参数、返回值类型一致),当表达式指向了一个方法引用的时候,数据绑定自动将方法引用和诉诸对象包裹在监听中,并将监听设置给目标 view ,如果表达式等于 null , 数据绑定不会创建监听,反之会将监听设 null 。
  • 监听绑定:即为事件触发时调用的 lambda 表达式。数据绑定总是会创建监听并且会 set 给 view 。当事件分发的时候,监听会直接调用 lambda 表达式。

方法引用

时间可以直接绑定到相应的处理方法上,与 android:onClick 可以绑定 activity 中的一个方法一样。根使用 View 的 onClick 参数相比,方法引用表达式的一个主要的优点就是他是在编译时进行处理的,如果方法不存在或者方法签名不正确的话我们在编译的时候就能发现。
方法引用与监听绑定的主要区别就在于,实际的监听实现是在数据绑定的时候就创建了,而不是在事件出发的时候。如果你更倾向于在事件发生的时候调用表达式,你应该使用监听绑定。
要将事件分配给其处理程序,使用一个普通的绑定表达式,其值为要调用的方法的名称:

1
2
3
public class MyHandlers {
public void onClickFriend(View view) { ... }
}

绑定表达式可以给 view 设置点击监听调用 onClickFriend() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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.OnClickListener.onClick() 方法一致

监听绑定

监听绑定是事件发生的时候执行的绑定表达式。与方法引用类似,但是可以让你执行任意的数据绑定表达式。这个功能在 Gradle 2.0 以后的对应的 Android Gradle Plugin 版本中可用。

在方法引用里,方法的参数和事件监听的回调参数必须一致。在使用监听绑定的时候,只需要确保返回值一致就行了。假设 Presenter 类有一个 onSaveClick() 方法:

1
2
3
public class Presenter {
public void onSaveClick(Task task){}
}

然后将事件绑定到 onSaveClick 方法上:

1
2
3
4
5
6
7
8
9
10
11
<?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>

当回调方法被用在了表达式上后,数据绑定类自动创建必要的监听并且注册到该事件上。当 view 分发事件的时候,数据绑定会直接调用给定的表达式。与常规的表达式一样,在调用这些监听表达式时候,你依旧可以获得线程安全和 null 保护。
上面的例子中,我们没有给 onSaveClick() 方法定义 onClick(View) 中的 view 参数。监听绑定提供两种监听参数:要么忽略所有参数,要么声明所有的参数。如果你更倾向于声明所有参数,你就可以在你的表达式中使用他们了(就是 lambda 表达式的写法)。

1
2
<!-- 你声明了 onClick 回调方法的所有参数(参数只有 view),但是你没有使用 -->
<android:onClick="@{(view) -> presenter.onSaveClick(task)}"/>

假如你想要在表达式中使用声明了的参数:

1
<android:onClick="@{(theViewINamed) -> presenter.onSaveClick(theViewINamed, task)}"/>

1
2
3
public class Presenter {
public void onSaveClick(View view, Task task){}
}

当然你也可以使用带有多个参数的 lambda 表达式

1
2
3
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}

1
2
3
// 即使在后面的表达式中只用了一个参数,也必须声明 onCheckedChanged 方法的所有参数
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

如果监听的事件回调函数有返回值的话,你的表达式也必须返回相同的类型。如果表达式因为 null 保护而无法被调用的话,数据绑定会默认但会该类型的默认值,引用类型为 nullint0booleanfalse
如果你需要在表达式中使用断言(例如三目运算符),你可以使用 void 作为占位:

1
<android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"/>

要避免复杂的监听

监听表达式非常强力,可以使你的代码简单易读。另一方面,包含复杂的表达式的监听可能是你的 layout 难于阅读和维护,这些表达式应该像 UI 向你自己的代码的监听传值一样简单。你应该在从侦听器表达式调用的回调方法中实现业务逻辑。

Imports ,variables 和 includes

数据绑定库也提供诸如 imports , variables , includes 的功能。Imports 可以让在布局文件中引用类更简单。Variables 可以使你描述细节并用到表达式中。Includes 可以让你复用复杂的布局。

Imports

可以引用类。例如导入了 android.view.View 类就可以使用 View.VISIABLE 和 View.GONE 了:

1
2
3
4
5
6
7
8
9
<data>
<import type="android.view.View"/>
</data>

<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

类型别名

当导入的类名有冲突的时候,我们可以给类指定别名:

1
2
3
4
<!-- 此时 View 指代 android.view.View ,使用 Vista 指代 com.example.real.estate.View -->
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>

导入其他类

导入的类可以作为其他类型的引用,下面这个例子展示了 User 类被 List 类用来做类型参数的情况:

1
2
3
4
5
6
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List&lt;User&gt;"/> // 没错,<>要转义
</data>

也可以用导入的类进行类型转换:

1
2
3
4
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

导入类可以直接调用静态方法和字段,下面的例子引用了 MyStringUtils 类的静态方法 capitalize() :

1
2
3
4
5
6
7
8
9
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>

<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Variables

你可以在 data 标签中使用多个 variable 元素。每个 variable 都描述了一个可以在 layout 表达式中能用到的属性。下面的代码声明了 userimagenote 变量:

1
2
3
4
5
6
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>

变量的类型会在编译时进行检查,因此如果变量实现了 Observable 或者是一个 Observable Colloction 的话,那他就会是可反射的。如果变量是一个基本类或者是一个没实现 Observable 的接口,那么这个变量就不是可观察的。
当布局文件根据不同配置发生改变的时候,变量时合并了的。这些布局文件中的变量绝对不可以有冲突。
生成的绑定类有对应每一个变量的 getter 和 setter ,直到 setter 被调用之前,变量都会使用其对应类型的默认值。
一个叫做 context 的特殊的变量被自动生成,以用来在表达式中使用。 context 的值相当于 root view 调用 getContext() 的返的 Context 对象。 context 会被显示声明的相同名称的变量所覆盖。

Includes

变量可以通过在 include 标签中使用 app 的命名空间并设置其对应的变量名来将变量传入到 include 的 layout 中。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</LinearLayout>
</layout>

数据绑定不支持 include 作为 merge 元素的直接子元素。例如,以下的布局是不支持的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="com.example.User"/>
</data>
<merge><!-- Doesn't work -->
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>

Work with observable data objects

可观察性(Observability)代表着在一个对象可以通知其他对象他的数据变化的能力。数据绑定库可以让你将对象、字段、集合变为可观察的(observable)。
任何普通的 Java 类都可以用来做数据绑定,但是变更这个对象的时候不会自动通知 UI 更新数据。数据绑定可以是你的对象在数据变化的时候通知其他对象,也就是通知他的监听器。有三种 observable 的类型,对象、字段和集合。
当以上任意一种 observable 的数据被绑定到 UI 上后,当其数据发生变化时,UI 也会自动更新。

Observable fields

创建实现了 Observable 接口的类需要做好些工作,这对于你那些只有几个字段的类来说并不友好。这种情况下,你可以使用通用的 Observable 类,下联就列举了基本类型的 observable 版本:

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

Observable 字段 自带了包含有一个字段的 observable 对象。原始版本在访问操作期间可以避免装箱和拆箱。为了利用这个机制,我们在 Java 语言中将其用 public final 修饰(为了保持其引用不变),在 Kotlin 中使用 val 修饰(只读):

1
2
3
4
5
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}

使用 set() get() 方法以访问其中的值:

1
2
user.firstName.set("Google");
int age = user.age.get();

Android Studio 3.1 及更高版本会建议你用 LiveData 来替换 observable 字段,详见 LiveData 文档

Observable collections

有些 app 使用动态的结构来保存数据。Observable collections 允许通过 key 来访问这些数据结构。ObservableArrayMap 类在 key 是引用类型的时候就很有帮助,例如 String ,示例如下:

1
2
3
4
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);

在 layout 文件中,这个 map 可以用 String 类型的 key 来访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 用于 key 是一个 Integer 的情况:

1
2
3
4
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);

在 layout 文件中即可用 index 来访问数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList<Object>"/>
</data>

<TextView
android:text='@{user[Fields.LAST_NAME]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Observable objects

实现了 Observable 接口的类可以让注册了的监听起们在数据变化的时候获得通知。
Observable 接口有添加和移除监听器的机制,但是你必须决定什么时候去发通知。为了使开发工作更简单,数据绑定库提供了已经实现了监听器注册机制的 BaseObservable 类。实现 BaseObservable 的类负责在数据变化的时候发出通知。通过给 getter 方法注解 Bindable 、然后在 setter 方法中调用 notifyPropertyChanged() 方法来实现,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class User extends BaseObservable {
private String firstName;
private String lastName;

@Bindable
public String getFirstName() {
return this.firstName;
}

@Bindable
public String getLastName() {
return this.lastName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}

public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}

数据绑定在对应的 module 中生成一个叫做 BR 的类(类似于 R ),其中包含了数据绑定的资源的 ID 。Bindable 注解在编译时在 BR 类中生成一个实体类。如果数据类的类型是不可变的,则可以使用 PropertyChangeRegistry 对象实现 Observable 接口,以高效的注册和通知监听器。

Generated binding classes

数据绑定库默认会生成类来访问 layout 里面的变量和视图。下面会展示如何创建和自定义绑定类。
生成的绑定类将 layout 中的变量和视图联系了起来。绑定类的包名和类名都可以自定义。所有的绑定类都继承于 ViewDataBinding 类。
每一个 layout 文件都会生成一个对应的绑定类。默认情况下,类名基于 layout 文件的名称进行驼峰命名并追加 Binding 后缀。上文的 layout 文件名为 activity_main.xml ,生成的类名即为 ActivityMainBinding 。这个类持有着所有的布局中视图信息,可以根据绑定的表达式给视图设置属性。

Create a binding object

绑定类对象紧接着在填充布局之后被创建,以确保视图在绑定之前不会被修改。将绑定类对象绑定到 layout 最常用的方法就是使用绑定类的静态方法。你可以通过调用绑定类的静态方法 inflate() 来同时完成填充布局和视图绑定。示例如下:

1
2
3
4
5
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
}

有一个 inflate() 的重载方法接收除了 LayoutInflater 之外的一个参数 – ViewGroup ,示例如下:

1
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false);

如果布局已经通过其他形式 inflate 了,可以如下进行单独的绑定:

1
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有些情况下,绑定类的类型无法事先得知,可以用 DataBindingUtil 来创建绑定:

1
2
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);

如果你结合 FragmentListView 或者 RecyclerView Adapter 使用数据绑定,你应该使用绑定类的 inflate() 方法,或者 DataBindingUtil 类,示例如下:

1
2
3
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

带有 ID 的 View

数据绑定库在绑定类中为每个在 layout 中带有 ID 的 View 创建了 immutable 的字段。例如数据绑定库会创建名叫 firstNamelastNameTextView 字段,在生成的绑定类中可以看到他们是被 final 修饰的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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 的 view 。这个机制可比给每个 View 都调用 findViewById() 快多了。
不使用数据绑定的话 ID 看起来没什么意义,但是仍然在有些情况需要在代码中调用。

Variables

数据绑定库为在布局中声明了的变量提供了访问方法。在下面的示例中,为 userimagenote 生成了对应的 getter 和 setter 方法:

1
2
3
4
5
6
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>

ViewStubs

与正常的 View 对象不同, ViewStub 对象最开始是一个不可见的 view 。当他们被设为可见或者被明确调用了 inflate() 方法的时候,他们才将自己替换成即将被渲染的 view 。
因为 ViewStub 最终会在视图层次中消失,绑定对象中的 view 为了能被垃圾回收也需要销毁。因为 view 是 final 的, ViewStubProxy 对象会在生成的绑定类中替代 ViewStub , 以此来在 ViewStub 存在的时候提供对 ViewStub 的访问,以及在 ViewStub inflate 之后提供对视图的访问。
当 inflate 另一个 layout 的时候,必须为新的布局建立绑定。因此, ViewStubProxy 必须监听 ViewStubOnInflateListener ,并且在需要的时候建立绑定。由于在给定的时间内只能存在一个监听器, ViewStubProxy 允许你设置 OnInflateListener , 并且在建立绑定之后得到回调。

Immediate Binding

当变量或者 observable 对象发生改变的时候,绑定会计划在下一帧之前生效。但是有些情况下需要立即进行绑定。要强制执行绑定,直接调用 executePendingBindings() 方法。

Advanced Binding

Dynamic Varaibles

有些情况下,指定的绑定类是未知的。举例来说, RecyclerView.Adapter 就不知到它该用何种绑定类(因为 Item Type 太多了),它得在调用 onBindViewHolder() 时分配绑定值。
在下面的例子里, RecyclerView 绑定的每一个的 layout 都对应着一个 item 变量。 BindingHolder 对象有一个 getBinding() 方法来返回基类 ViewDataBinding

1
2
3
4
5
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}

Background Thread

只要你的数据对象不是集合类,你就可以随意的在后台线程更改你的数据。数据绑定会在调用期间本地化每一个变量以此来避免并发问题。

Custom binding class names

默认情况下,绑定类的名称是根据 layout 文件的文件名来决定的。生成的绑定类位于当前 module 包名下的 databinding 包中。

绑定类可以通过 data 元素中的 class 参数来指定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- module.package.name.databinding.ContactItem -->
<data class="ContactItem">

</data>

<!-- module.ppackage.name.ContactItem -->
<data class=".ContactItem">

</data>

<!-- com.example.ContactItem -->
<data class="com.example.ContactItem">

</data>

Binding adapters

Binding adapters 负责对想要适当的值进行框架层面的调用。例如通过调用 setText() 方法设置一个合适的值,或是调用 setOnClickListener() 设置监听。
数据绑定允许你通过使用 binding adapters 来指定方法进行赋值,提供自定义的绑定逻辑,指定返回对象的类型。

Setting attribute values

无论绑定的值如何变化,生成的绑定类必须要借助绑定表达式对 view 调用一个 setter 方法。你可以让数据绑定库自定决定调用何种方法,或是指定方法,再或是提供逻辑来指定方法。

Automatic method selection

举个例子,如果 view 的一个参数叫做 example , 绑定库会自动去寻找能接受对应类型参数的 setExample(arg) 方法。命名空间不会被使用到,只有参数名和类型会被用来查找对应的方法。
例如, android:text="@{user.name}" 表达式中,数据绑定库会去查找 setText(arg) 方法,如果 user.getName() 的返回类型是 String ,数据绑定库就会去查找 setText(String arg) 方法,如果 user.getName() 的返回类型是 int ,数据绑定库就回去查找 setText(int arg) 方法。表达式的返回值类型必须相符,必要时可以使用 cast 转换。
在没有对应参数的情况下,数据绑定也可以正常工作。你甚至可以通过创建对应的 setter 函数来增加 xml 的参数。例如, Support 包下的 DrawerLayout 并没有什么特别的 xml 参数,但是他有好多 setter 函数。数据绑定库就会自动根据 app:scrimColorapp:scrimColor 参数来调用 setScrimColor(int)setDrawerListener(DrawerListener) 方法:

1
2
3
4
5
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">

Specify a custom method name

有些参数和它对应的 setter 方法名字并不匹配。在这种情况下,参数和对应的 setter 函数可以通过 BindingMethods 注解关联。注解用来修饰类,一个类可以有多个注解,每个注解声明了一个参数和 setter 的关联。例如 android:tint 对应的是 setImageTintList(ColorStateList) 而不是 setTint() 方法,这是我们就可以这么干:

1
2
3
4
5
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})

大多数情况下, Android 提供的类都不需要做重命名的映射,他们都进行了自动的方法匹配。

Provide custom logic

有些参数需要自定义的逻辑。例如, android:paddingLeft 没有对应的 setPaddingLeft(left) 方法,但是却提供了 setPadding(left, top, right, bottom) 方法。一个带有 BindingAdapter 注解的静态方法就可以让你完成自定义参数逻辑:

1
2
3
4
5
6
7
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}

Android 提供的类也已经创建了相应的 BindingAdapter 注解。
上述代码参数类型很重要,第一个参数的类型决定了要在哪种类上关联参数,第二个参数的类型对应着表达式里面的类型。
BindindAdapters 可以用来做各种类型的自定义。例如,你可以在工作线程调用自定义的加载程序来加载图像。
当存在冲突的时候,你自定义的 adapter 会覆盖 Android 框架的 adapter 。
你也可以用 adapter 同时接收多个参数:

1
2
3
4
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}

相应地,你可以这样使用你定义的参数:

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

上面的例子中,只有当两个参数都被设置了的时候, adapter 才会生效,如果你想在仅有一个参数的情况下也使用 adapter , 你可以吧注解的参数 requireAll 设置成 false

1
2
3
4
5
6
7
8
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

Bindging adapter 方法还可以接收旧值作为参数。接收新旧参数的方法应该先声明所有的旧参数然后在声明新的参数,示例如下:

1
2
3
4
5
6
7
8
9
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}

事件处理程序只能与只有一个抽象方法的接口或抽象类一起使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// View.OnLayoutChangeListener 接口只有一个方法。如果借口或者抽象类有多个方法的话,数据绑定库就知道该调用哪个方法了。
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListeneroldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}

在 layout 中这样使用:

1
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

当监听类有多个方法时,只能将它分成多个接口。例如, View.OnAttachStateChangeListener 中有两个方法:onViewAttachedToWindow(View)onViewDetachedFromWindow(View) ,你必须创建两个接口来然后使用 BindingAdapter 创建两个参数,然后对应两个监听器:

1
2
3
4
5
6
7
8
9
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}

因为改变一个监听器会影响另一个,你得需要一个能同时管理两个监听的 adapter 。你可以设置 BindingAdapter 注解的 requireAll 参数为 false , 这样不用每次都得指定指定两个监听了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}     requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
// 如果 attach 监听和 detach 监听都为空的话,那就不需要给 view 设置 OnAttachStateChangeListener 了
newListener = null;
} else {
// 设置监听
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}

// 获取旧的 OnAttachStateChangeListener
OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,newListener,
R.id.onAttachStateChangeListener);
// 移除旧的 OnAttachStateChangeListener
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}

// 设置新的 OnAttachStateChangeListener
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}

上述示例要比正常的稍微复杂一些,因为 View 类用的是 addOnAttachStateChangeListener()removeOnAttachStateChangeListener() 而不是 onAttachStateChangeListener 的 setter 方法。 android.databinding.adapters.ListenerUtil 这个类记录了以往了监听,以便在需要的时候用来移除。
通过对 OnViewDetachedFromWindowOnViewAttachedToWindow 设置 @TargetApi(VERSION_CODES.HONEYCOMB_MR1) 注解,绑定类生成器就知道只有在 API 12 以上才会生成相应的监听器,这个机制与 addOnAttachStateChangeListener() 相同。

Object conversion

Automatic object conversion

当绑定表达式返回一个 Object 时,由绑定库来调用相应的方法设置参数。 Object 对象会被根据方法的参数类型自动转型。这个行为在配合使用 ObservableMap 的时候尤其便利:

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

在绑定表达式中使用的 userMap 对象会返回一个值,会被自动转为 android:text 所对应的 setText(CharSequence) 函数的的参数类型。如果参数类型不明确的话,必须要在表达式中手动转型。

Custom conversions

在有些情况下,对于某些特定的类型必须要进行手动转型。例如, android:background 参数期待的是一个 Drawable 参数,但是 color 的值却是一个数字。下面的例子就展示了这种情况:

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

当期待的是 Drawable 但是提供的却是 int 时, int 就需要被转为其颜色对应的 ColorDrawable 。转换可以通过给静态方法声明 BindingConversion 注解:

1
2
3
4
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}

但是,表达式里面提供的类型必须是一致的,不可以在同一个表达式里面返回不同的类型:

1
2
3
4
<View
android:background="@{isError ? @drawable/error : @color/white}" // 不可以的
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Bind layout views to Architecture Components

AndroidX 库中包含 Architecture Components , 你可以用来设计健壮、可测试可维护的 APP 。数据绑定库可以和架构组件库无缝协作,进一步简化 UI 的开发。 APP 中的布局可以绑定架构组件的数据,可以帮助您管理 UI 控制器的生命周期和通知数据中的更改。

Use LiveData to notify the UI about data changes

你可以使用 LiveData 对象作为数据绑定的数据源,来通知 UI 数据发生了更改。详细信息见 LiveData 的文档。
不像实现了 Observable 的对象, LiveData 知道订阅数据变更通知的观察者的生命周期。这带来了许多好处,在 The advantages of using LiveData 中有详细说明。在 Android Studio 3.1 以及更高版本中,你可以用 LiveData 来替换代码中的 observable fields 。
要在你的绑定类中使用 LiveData ,你需要指定一个生命周期所有者来定义 LiveData 对象的范围。下面的例子展示了在绑定类被初始化之后指定了 activity 作为生命周期所有者:

1
2
3
4
5
6
7
8
9
10
class ViewModelActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this);
}
}

你可以使用 ViewModel 组件,来将数据绑定到 layout 上。在 ViewModel 组件中,你可以使用 LiveData 来转换并且处理多个数据源。示例如下:

1
2
3
4
5
6
7
class ScheduleViewModel extends ViewModel {
LiveData username;

public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}

数据绑定库可以和 ViewModel 组件无缝衔接, ViewModel 会将数据暴露给 layout 并对数据变化做出响应。结合 ViewModel 使用数据绑定可以将你的布局逻辑移至 ViewModel 中,这样可以更容易测试。数据绑定库负责在相应的时候绑定和解绑数据源。剩下的大部分工作都在于确保你暴露的数据的正确性。
要将 ViewModel 组件与数据绑定库一起使用,必须实例化继承了 ViewModel 的组件,再获取绑定类的实例,并将 ViewModel 示例赋值给绑定类相应的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ViewModelActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Obtain the ViewModel component.
UserModel userModel = ViewModelProviders.of(getActivity())
.get(UserModel.class);

// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

// Assign the component to a property in the binding class.
binding.viewmodel = userModel;
}
}

在 layout 中,使用绑定表达式将 ViewModel 中的字段和方法用在对应的 view 上:

1
2
3
4
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

Use an Observable ViewModel for more control over binding adapters

你可以使用 ViewModel 实现 Observable 接口来同时 UI 数据变更,类似于使用 LivaData。
在某些情况下,你可能会更偏向于使用 ViewModel 组件实现 Observable 接口,而不是直接使用 LiveData 对象,即使这意味着你得舍弃 LiveData 的生命周期管理功能。使用 ViewModel 实现 Observable 借口可以给予你 Binding Adapter 的更多控制权。下面的例子就能在数据变化的时候更好的控制通知,还允许你自定义方法在双向绑定中自定义参数的值。
要实现一个可观察的 ViewModel ,你先要继承 ViewModel 组件并实现 Observable 接口。当观察者使用  addOnPropertyChangedCallback()removeOnPropertyChangedCallback() 订阅或者取消订阅时,你可以提供自定义逻辑。你还可以提供在 notifyPropertyChanged() 方法中属性更改时的自定义逻辑。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
class ObservableViewModel extends ViewModel implements Observable {
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

@Override
protected void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.add(callback);
}

@Override
protected void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}

/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}

/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}