深入Android Data Binding(一):使用详解

本篇文章翻译自官方介绍文档(原文

本篇文章介绍了如何在Android开发中使用Android的Data Binding库来减少开发中的胶水代码(组织布局和业务逻辑的代码)。
Data Binding库支持在Android 2.1(API level 7)及以上版本中使用。
使用Data Binding,Gradle的Android插件需要使用1.5.0-alpha1及以上版本。具体配置可参考如何更新Gradle Android插件?

Data Binding运行环境

要使用Data Binding,首先需要在Android SDK中下载Data Binding的支持包。
然后在应用的构建文件中配置data binding选项,项目的build.gradle文件中配置如下:

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

如果你的应用模块依赖的Library模块需要使用data binding,那么在应用模块的build.gradle文件中也要配置data binding。
最后,你需要使用Android Studio 1.3及以上版本,因为这些版本对data binding才有较好的支持,详情可查看Android Studio对Data Binding的支持

Data Binding布局文件修改

你的第一个Data Binding表达式

Data-binding的布局文件与常规的布局文件有一点不同,其根标签是layout标签,然后紧跟着data标签,最后才是常规的布局标签。样例可参考如下:

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标签里的variable属性user,会在ui布局中使用到

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

在ui的描述标签中通过使用“@{}”的语法来使用data中申明的变量的属性。如在TextView中将user的firstName设置给TextView的text属性

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

Data Object

假设你现在有一个简单的User Java对象(POJO)

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;
}
}

这种对象的数据通常一直都不会变,在应用开发中有很多场景都会使用到这种不可变对象。你也可以使用JavaBean对象:

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;
}
}

对于data binding来说,以上两种方式是一样的。android:text会获取在表达式@{user.firstName}中定义的firstName属性,在上面描述的第一种方式对应的就是user.firstName变量,而在第二种方式则对应的是getFirstName()方法,如果方法中有firstName()方法存在,data binding也会将其对应为firstName属性

如果getFirstName()和firstName()同时存在,会先取getFirstName()方法的内容,getFirstName()方法没有才会去firstName()方法。

绑定数据

绑定类默认会根据布局文件的名称按照“layout文件名的Pascal名称+Binding”的格式生成,如:布局文件名称为main_activity.xml,则生成的的类名称为MainActivityBinding.这个类包含了layout中定义的所要绑定的内容(如:User变量),Binding类知道如何将变量的值对应到layout中引用的表达式中。绑定数据最简单的方法就是在infalte的过程中操作:

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

以上绑定类生成和设置变量的所有流程。再次获取binding对象可通过如下方式

1
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你在ListView或者RecyclerView的adapter中绑定列表的Item,可以这样使用

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

事件处理

Data Binding允许表达式处理view各种分发的事件(如:onClick监听)。除了一小部分特殊情况外,事件的属性通常是view的listener的方法名称,如view.OnLongClickListener对应的方法为onLongClick()。在XML的表达式的事件处理则为android:onLongClick。在Data Binding中有如下两种方式来做事件处理。

  • 方法引用(Method Reference):在你的事件表达式中,你可以引用符合监听器方法签名的处理方法。 当表达式求值为方法引用时,Data Binding会在监听器中封装方法的引用和其所有者对象,并在目标View上设置该监听器。 如果表达式的值为null,则数据绑定不会创建监听器,而是设置一个空监听器。

  • 监听器绑定(Listener Bindings):此种方式的事件表达式是一个lambda表达式。通常lambda表达式会在在view的事件发生时再生效。Data Binding默认总是创建一个监听器,并设置在对应View上。当view分派事件时,监听器再计算lambda表达式的值,最终作用在lambda表达式中声明的处理方法上。

方法引用(Method Reference)

事件可以直接绑定到处理程序方法,类似于android:onClick的方式可以分配到Activity中的一个方法。 与View#onClick属性相比,一个主要优点是表达式在编译时处理,因此如果方法不存在或其签名不正确,你会收到编译时错误。

方法引用和监听器绑定之间的主要区别是,方法引用中的实际监听器实现是在数据绑定时创建的,而不是在触发事件时创建的。如果你喜欢在事件发生时再计算表达式,则应使用监听器绑定方式。

要将事件分配给其处理程序,需要使用正常绑定表达式,其值为要调用的方法名称。 例如,如果你的事件处理对象如下所示:

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

绑定表达式可以为view分配点击监听器:

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.Handlers"/>
<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>

请注意,表达式中的方法的签名必须与监听器对象中方法的签名完全一致。

监听器绑定(Listener Bindings)

监听器绑定是在事件发生时才运行的绑定表达式。它类似于方法引用,但是允许你运行任意数据绑定表达式(不限制处理方法参数)。此功能适用于Android Gradle插件2.0及更高版本。

在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只要你的返回值与监听器的预期返回值匹配(除非它期望void)即可。例如,你可以有一个presenter类,它具有以下方法:

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

然后,你可以将点击事件绑定到你的Presenter类中,如下所示:

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>

监听器由lambda语句表示,并且lambda语句只允许作为表达式的根元素来使用(译者注:这里可以理解为在箭头左右的语句中不能再使用lambda语句)。Data Binding自动为事件创建监听器或注册者,当View触发事件时,Data Binding再计算给定的表达式。在常规绑定表达式中,在计算这些监听器表达式时,Data Binding会保证绑定表达式中引用变量的空值安全和线程安全性。

注意,在上面的示例中,我们没有定义传递到onClick(android.view.View)的View参数。监听器绑定方式为监听器参数提供两个选择:你可以忽略方法的所有参数或命名出所有参数出来。如果你喜欢命名参数,可以在表达式中使用它们。 例如,上面的表达式可以写成:

1
android:onClick="@{(view) -> presenter.onSaveClick(task)}"

如果你想使用表达式中的参数,如下所示:

1
2
3
public class Presenter {
public void onSaveClick(View view, Task task){}
}
1
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

你还可以使用具有多个参数的lambda表达式:

1
2
3
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}
1
2
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

如果正在监听的事件返回类型不是void的值,则表达式也必须返回相同类型的值。 例如,如果你想监听长点击事件,你的表达式应该返回布尔值。

1
2
3
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
1
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果由于空对象而无法计算表达式,Data Binding将返回该类型的默认Java值。 例如,引用类型为null,int为0,boolean为false等。

如果需要使用带断言(例如三元表达式)的表达式,则可以使用void作为空操作符号。

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

避免复杂的监听器
监听器表达式非常强大,可以使你的代码容易阅读理解。另一方面,如果包含复杂表达式的监听器会使你的布局难以阅读和维护。这些表达式应该像将数据从UI层传递到处理方法中一样简单,由监听器表达式调用的处理方法来实现所有的业务逻辑。

有一些特殊的单击事件处理程序可能会与View.OnClickListener的onClick(View)方法冲突,所以他们需要另外一个属性来代替而不是android:onClick方法,从而来避免冲突。下表是为避免此类冲突而创建的特殊属性:

类名 Listener设置方法 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

布局详解(Layout Details)

导入(Imports)

在数据元素内可以使用零个或多个导入元素。 导入语句允许像Java一样在布局文件中引用类。

1
2
3
<data>
<import type="android.view.View"/>
</data>

现在,在你的绑定表达式中可以使用View:

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

当有类名冲突时,其中一个类可以重命名为“alias:”

1
2
3
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>

现在,Vista可以用于引用com.example.real.estate.View而View可以用于在布局文件中引用android.view.View。 导入类型可以用作变量和表达式中的类型引用:

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<User>"/>
</data>

注意:Android Studio尚不能自动处理导入,因此导入的变量的自动完成功能当前无法在IDE中工作。你的应用程序可能仍然会编译良好,但是运行时可能会引起错误,因此你可以通过在变量定义中使用完全限定名来解决IDE的问题。

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

在引用表达式中的静态字段和方法时,也可以使用导入类型:

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"/>

跟Java中一样,java.lang.*是自动导入的。

变量(Variables)

在数据元素内部可以使用任何数量的变量。变量的每一个属性都可以作为布局文件中绑定表达式使用的属性。

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>

Data Binding会在编译时检查变量类型,因此如果变量实现了Data Binding中的Observable接口或者是一个Observable集合接口,那么还需要在对应变量属性上加上标示。如果变量没有实现Observable*接口的基类或接口,变量不会被观察!

当应用中存在用于各种配置(例如,横向或纵向)的不同布局文件时,变量将被组合。 这些布局文件之间不能有冲突的变量定义。

生成的绑定类具有每个描述的变量的setter和getter方法。 在变量的setter方法调用之前,变量将采用默认的Java值:引用类型为null,int为0,boolean为false等。

Data Binding默认会生成一个名为context的特殊变量,以根据需要用于绑定表达式。context的值是来自根View的getContext()的context。context变量将被具有该名称的显式context声明覆盖。

自定义Binding类名称(Custom Binding Class Names)

默认情况下,Binding类是基于布局文件的名称生成的,以大写字母开头,除去下划线(_),然后加上“Binding”。 这个类将被放置在模块包下的数据绑定包中。 例如,布局文件contact_item.xml将生成ContactItemBinding。 如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding中。

通过调整data元素的class属性,绑定类可以重命名或放置在不同的包中。 例如:

1
2
3
<data class="ContactItem">
...
</data>

这将在模块包中的数据绑定包中生成绑定类作为ContactItem。 如果类应该在模块包内的不同包中生成,则可以使用“.”作为前缀。

1
2
3
<data class=".ContactItem">
...
</data>

在如下示例中,ContactItem直接在模块包中生成。 如果提供完整包名,则绑定类就会生成在指定包名下:

1
2
3
<data class="com.example.ContactItem">
...
</data>

布局引用(Includes)

data中声明的变量可以通过bind属性将其传递到被include的布局中:

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>

上面的代码中,在name.xml和contact.xml布局文件中都必须有一个用户变量。

数据绑定不支持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>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>

表达式语法(Expression Language)

通用语法

通用语法跟在Java中使用的大多类似,如下所示:

  • Mathematical(数学运算符) + - / * %
  • String concatenation(字符拼接) +
  • Logical(逻辑运算符) && ||
  • Binary(位运算符) & | ^
  • Unary(一元操作符) + - ! ~
  • Shift(位移运算) >> >>> <<
  • Comparison(判断) == > < >= <=
  • instanceof (实例判断)
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator(三目运算符) ?:

实例:

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

不支持的操作符

在Java中支持但是在Data Binding的表达式语法不支持的是:

  • this
  • super
  • new
  • Explicit generic invocation(显式泛型调用)

空合并运算符(Null Coalescing Operator)

空合并运算符(??):如果左操作数不为空,则选择左操作数否则选择右操作数。

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

上面等价于

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

属性引用(Property Reference)

在第一节已经讨论过:JavaBean引用简写。对于类变量:fiels,getters或者ObservabeFiels方式都是一样的

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

空指针异常处理(Avoiding NullPointerException)

生成的数据绑定代码会自动检查null并避免空指针异常。 例如,在表达式@ {user.name}中,如果user为null,则将为user.name分配其默认值(null)。 如果你引用了user.age,其中age是一个int,那么它将默认为0。

集合(Collections)

为了方便,可以使用[]运算符来访问公共集合:数组,List,sparse list和Map。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<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]}"

字符语法(String Literals)

当属性值使用单引号引用时,表达式中需使用双引号:

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

也可以使用双引号引用属性值,字符串文字应该使用 或后引号 `

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

资源(Resources)

可以使用正常语法来访问资源:

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

格式字符串和复数可以使用参数:

1
2
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
1
2
3
4
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"

一些资源需要在表达式中使用特定类型。

类型 正常类型 表达式类型
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

数据对象(Data Objects)

所有普通的Java对象(POJO)都可以用于数据绑定,但是修改POJO不会导致UI更新。通过为数据对象提供数据更改时通知的能力,可以使用Data Binding的真正强大功能。 有三种不同的数据更改通知机制,可观察对象(Observable objects),可观察字段(Observable fields)和可观察的集合(Observable collections)。

当这些可观察的数据对象绑定到UI,当数据对象的属性更改时,UI将会自动更新。

可观察对象(Observable Objects)

实现Observable接口的类允许将单个listener附加到绑定对象上,以监听该对象上所有属性的更改。

Observable接口具有添加和删除listener的机制,但通知动作是由开发人员决定的。 为了使开发更容易,Data Binding库创建了一个BaseObservable基类来实现监听器注册机制。 数据类实现者仍然负责通知属性何时更改。 通过分配一个Bindable Annotation到getter方法同时在setter方法中发送通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

Bindable注解编译期间将在模块包中生成BR类文件,同时在BR类文件中生成一个条目。如果数据类的基类不能被改变,则Observable接口可以使用方便的PropertyChangeRegistry来实现,以有效地存储和通知监听器。

可观察属性(ObservableFields)

有一些工作涉及到创建Observable类,所以开发人员想要节省时间或少量属性可以使用ObservableField及ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble和ObservableParcelable。 ObservableField其实是具有单个字段的自包含ObservableObject。 基本类型的观察对象避免了在访问操作期间的装箱和拆箱操作。 使用时,在数据类中创建对应的public final字段:

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

取值时,使用取值方法:

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

可观察集合(Observable Collections)

一些应用程序使用更多动态数据结构来保存数据。可观察集合包装了对这些数据对象的操作。 当键是引用类型(例如String)时,建议使用ObservableArrayMap。

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

在布局文件中,可以通过String的key访问map:

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类似于Java中的ArrayList:

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

在layout文件中,可以通过索引访问列表:

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"/>

生成绑定类(Generate Binding)

生成的绑定类将布局变量与布局中的视图相关联。 如前所述,绑定的名称和包可以自定义。 生成的绑定类都继承自ViewDataBinding。

创建(Creating)

绑定类应该在view inflate后立即创建,从而确保在布局中的表达式绑定到视图之前,View层次结构不会受到干扰。 有几种方法绑定到布局。 最常见的是在Binding类上使用静态方法。inflate方法一次性填充Layout并绑定到Layout中View的所有层次结构。 有一个更简单的版本,只需要一个LayoutInflater和一个还需要一个ViewGroup:

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

如果布局是使用不同的机制inflate,则可以单独绑定:

1
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

有时,绑定不能提前知道。 在这种情况下,可以使用DataBindingUtil类创建绑定:

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

带Id的View(Views with IDs)

将在布局中为每个具有Id的每个视图生成公共final字段。 绑定在View层次结构上执行单次传递,使用Id来提取视图。 这个机制可能比为几个视图调用findViewById更快。 例如:

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会在binding类中生成如下字段

1
2
public final TextView firstName;
public final TextView lastName;

Data Binding中几乎没有使用Id的必要,但仍然有一些情况下可能需要从代码访问视图。

变量(Variables)

每一个变量都会被赋予访问方法

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>

在binding类中会生成set和get方法

1
2
3
4
5
6
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
public abstract Drawable getImage();
public abstract void setImage(Drawable image);
public abstract String getNote();
public abstract void setNote(String note);

ViewStub

ViewStub与普通View有点不同。 它们从不可见状态开始,当它们被显示或被明确告诉inflate时,他们在布局中通过inflate另一个布局来替换自己。

因为ViewStub基本上从View层次结构中消失,所以绑定对象中的View也必须消失以允许回收。 因为View是final的,所以采用ViewStubProxy对象代替ViewStub,当ViewStubProxy存在时,它允许开发人员通过它来访问ViewStub,并且当ViewStub被inflate后,还可以访问其填充后的View层次结构。

当inflate另一个布局时,必须为新布局建立绑定。 因此,ViewStubProxy必须监听ViewStub的ViewStub.OnInflateListener并在那时建立绑定。 由于只有一个可以存在,ViewStubProxy允许开发人员设置一个OnInflateListener,它将在建立绑定后调用。

高级绑定(Advanced Binding)

动态变量(Dynamic Variables)

有时,绑定类不一定知道具体的变量类型。 例如,对任意布局操作的RecyclerView.Adapter不会知道绑定类的数据类型。 它仍然必须在onBindViewHolder(VH,int)期间分配绑定值。

在如下示例中,RecyclerView绑定的所有布局都有一个“item”变量。 ViewHolder的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();
}

快速绑定(Immediate Binding)

当变量或可观察数据变化时,系统调度绑定类将在下一帧之前改变。 但是,如果必须立即执行绑定,可以强制执行,使用executePendingBindings()方法。

后台线程(Background Thread)

你可以在后台线程中更改非集合的数据模型。Data Binding会在计算时将每个变量/字段在各个线程做一份数据拷贝,以避免任何并发问题。

xml属性设置器(Attribute Setters)

每当Data的变量数据更改时,绑定类必须将更改的值设置的对应的View的属性中去。Data Binding框架提供了多种方式来实现View属性的设置

自动设置器(Automatic Setters)

对于View中的属性,Data Binding会直接查找对应的set方法。属性的命名空间无关紧要,只有属性名称本身。

例如:android:text=user.age,与TextView的属性相关联的表达式将查找setText(String)。 如果user.age是一个int型数据,Data Biding将搜索一个setText(int)方法。所以表达式需要返回正确的类型,如果必要,需要进行格式的转换,(这里需要注意的是,即使View中没有给定名称的属性,Data Binding依然会工作)。你可以通过使用Data Binding轻松地为任何View中的任何set方法“创建”属性。 例如,support包下的DrawerLayout没有任何xml属性,但有大量的set方法。 你可以使用自动设置器来使用其中的任何一个。

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}"/>

重命名设置器(Renamed Setters)

某些View的set方法跟xml中的属性不一致。例如ImageView有一个setImageTintListener(ColorStateList tint)的方法,但是xml文件中对应的描述属性是android:tint,对于这些方法,属性可以通过BindingMethods注解与设置器相关联。 这必须与一个类相关联,并包含BindingMethod注释,每个重命名方法一个。 例如,android:tint属性与setImageTintList(ColorStateList)相关联,而不是setTint。

1
2
3
4
5
6
7
8
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
public class ImageView extends View{
public setImageTintListener(ColorStateList tint){}
}

对于Framework层的空间属性,大部分已经实现了DataBinding属性名,不需要开发者自己重命名Framework层的控件

自定义设置器(Custom Setters)

一些属性需要自定义绑定逻辑。 例如,没有为android:paddingLeft属性的关联set方法,而只存在setPadding(left,top,right,bottom)方法。 使用BindingAdapter注解的静态绑定适配器方法允许开发人员自定义属性调用的set方法。

android属性已经创建了paddingLef的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());
}

BindingAdapter也可用于自定义其他类型。 例如,可以调用自定义加载器以加载离线图片。

当自定义属性和系统属性发生冲突时,开发人员创建的自定义BindingAdapter将覆盖Data Binding的默认适配器。

绑定适配器还可接收多参数

1
2
3
4
@BindingAdapter({"bind:imageUrl", "bind:error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
1
2
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>

如果imageUrl和error都用于ImageView,而imageUrl是字符串并且error是drawable,则调用此适配器。

  • 在匹配期间将忽略自定义命名空间。
  • 你也可以为android命名空间编写适配器。

绑定适配器方法可以选择地在其处理程序中采用旧值。 采用旧值和新值的方法应该具有属性的所有旧值,然后是新值:

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
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}

当侦听器有多个方法时,它必须拆分成多个侦听器。 例如,View.OnAttachStateChangeListener有两个方法:onViewAttachedToWindow()和onViewDetachedFromWindow()。 然后,我们必须创建两个接口来区分它们的属性和处理程序。

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);
}

因为改变一个监听器也会影响另一个监听器,所以我们必须有三个不同的绑定适配器,一个用于每个属性,一个用于两个属性,它们都应该被设置。

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
40
41
42
43
44
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
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);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}

上面的例子比正常情况稍微复杂一些,因为View使用add和remove作为监听器,而不是View.OnAttachStateChangeListener的set方法。 android.databinding.adapters.ListenerUtil类可以帮助跟踪以前的监听器,以便它们可以在绑定Adapter中删除。

通过用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注解接口OnViewDetachedFromWindow和OnViewAttachedToWindow,数据绑定代码生成器知道监听器应该只在Honeycomb MR1和新设备上运行时生成。

同理addOnAttachStateChangeListener(View.OnAttachStateChangeListener)与上述一样。

转换器(Converters)

对象转换(Object Conversions)

当从绑定表达式返回对象时,将从自动,重命名和自定义set方法三种方式中选择一个set方法。 对象将被转换为所选择的set方法的参数类型。

比如使用ObservableMaps来保存数据:

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

userMap根据key值获取到一个对象,该对象将被自动转换为在setText(CharSequence)方法中的参数类型。 当可能存在关于参数类型歧义时,开发人员需要在表达式中显示转换。

自定义类型转换(Custom Conversions)

有时,类型应在特定类型之间自动进行。 例如,当设置背景时:

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

上面代码中,View的背景应该采用Drawable,但是颜色是数值代表的。 每当一个Drawable被期望并返回一个整数,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"/>

Android Studio对Data Binding的支持

Android Studio支持许多用于Data Binding代码的编辑功能。 例如,它支持Data Binding表达式的以下功能:

  • 语法高亮
  • 标记表达式语法错误
  • XML代码自动提示
  • 资源引用,包括导航(如导航到声明)和快速文档

注意:数组和泛型类型(如Observable类)可能在没有错误时显示错误。

布局“preview”窗口显示Data Binding的默认值(如果有的话)。 在以下示例中,从布局XML文件中截取元素,“preview”窗口在TextView中显示PLACEHOLDER默认文本值。

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

如果需要在项目的设计阶段显示默认值,还可以使用tools:属性而不是默认表达式值

写得好,就打赏一下吧!