本内容主要介绍如何使用 Android 架构组件中的 Data Binding。
Android DataBinding 的官方介绍文档:https://developer.android.com/topic/libraries/data-binding/
Data Binding 库是一个支持库,它允许您在 App 中通过声明方式(而不是编程方式)将布局中的 UI 组件和数据源进行绑定。
Data Binding 库具备灵活性和兼容性,可以在 Android 4.0(API 14)以及更高版本中使用。
在 Android Gradle 插件的 1.5.0 以及更高版本中支持 Data Binding 库,不过推荐使用最新版本。
为了在您的 App 中使用 Data Binding,需要在您的 App 模块的 build.gradle
文件中添加 dataBinding
元素,如下所示:
android {
...
dataBinding {
enabled = true
}
}
注意:即使您 App 模块没有直接使用 Data Binding,但是如果它的依赖库使用了 Data Binding,那么也需要像上面一样配置
dataBinding
元素。
在 Android Gradle 插件的 3.1.0-alpha06
版本中包含了一个生成绑定类的新 Data Binding 编译器。新的编译器逐步创建绑定类,这在大多数情况下加快了构建过程。
以前版本的 Data Binding 编译器在编译代码时生成绑定类。如果您的代码编译失败,您可能会收到多个报告绑定类无法找到的错误。新的 Data Binding 编译器将在编译代码之前生成绑定类,来避免这些错误。
要启用新的 Data Binding 编译器,请在 gradle.properties
文件中添加以下选项:
android.databinding.enableV2=true
你还可以通过在 gradle 命令中添加以下参数来启用新的编译器:
-Pandroid.databinding.enableV2=true
注意:Android Gradle 插件版本 3.1 中的 Data Binding 编译器不向后兼容。。然而,Android Gradle 3.2 版本中的新编译器与先前版本生成的绑定类兼容。在 3.2 版本中,默认启用新编译器。
表达式语言允许您编写表达式,用于处理 View 分发的事件。Data Binding 库将自动产生一个类,用于将布局中的 View 与数据对象绑定。
Data binding 布局文件略有不同,最外面是 layout
的根标记,里面包含一个 data
元素和一个 view
根元素。这个 view
元素实际上就是非 Data binding 布局文件的根元素。下面的代码显示了一个示例布局文件:
<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
变量,在布局中可以使用它。
布局中的表达式使用 "@{}"
语法写入属性中。上面的 TextView
的 text 被设置为 user
变量的 firstName
属性。
注意:布局表达式应该小而简单,因为它们不能进行单元测试,并且 IDE 对其支持有限。您可以使用自定义的 Binding Adapter 来简化布局表达式。
假设你有一个 POJO(Plain Old Java Object) 来描述 User
实体:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这类对象持有的数据永不改变。也可以使用遵循一组约定的对象,如下面的示例所示:
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
字段和第二个类的 getFirstName()
(用 firstName()
替换也可以)方法。
为每个布局文件生成一个 Binding 类。默认情况下,基于布局文件的名称给这个 Binding 类命名,采用“帕斯卡命名法”(即大驼峰命名法)并添加 Binding 后缀。比如,布局文件的名称为 activity_main.xml
,则对应的 Binding 类的名称为 ActivityMainBinding
。这个 Binding 类拥有从布局属性(例如 user
变量)到布局中的 View 的所有 Binding,并知道如何为绑定表达式赋值。推荐在 inflate 布局时创建 Binding,如下面的代码所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding
= DataBindingUtil.setContentView(this, R.layout.activity_main);
User user = new User("Test", "User");
binding.setUser(user);
}
你也可以使用 LayoutInflater 获取 View,如下面的代码所示:
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
如果在 Fragment、ListView 或 RecyclerView Adapter 中使用 Data Binding,您可以使用 Binding 类的 inflate()
方法或 DataBindingUtil 类。如下面的代码所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(
layoutInflater, R.layout.list_item, viewGroup, false);
在表达式语言中,您可以使用如下运算符和关键字:
+ - / * %
+
&& ||
& | ^
+ - ! ~
>> >>> <<
== > < >= <=
(注意:<
需要转义为 <
)instanceof
()
null
[]
?:
例子:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
在表达式语法中,不能使用以下操作:
this
super
new
以下两种方法等效:
android:text="@{user.displayName ?? user.lastName}"
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
表达式可以按照以下格式引用类的属性,引用方法对字段、getter 和 ObservableField
对象都是相同。
android:text="@{user.lastName}"
生成的 Data Binding 代码会自动检查 null
值,从而避免空指针异常。例如,在表达式 @{user.name}
中,如果 user
为 null
,user.name
将被赋为默认的 null
。如果您引用 user.age
,因为 age
是 int
类型,那它的默认值为 0
。
为了方便,可以使用 []
运算符访问常用集合(比如 array、list、稀疏 list 和 map)。
<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]}"
注意:需要转义 < 字符,来确保 XML 语法正确。所以需要使用
List<String>
来代替List
。
您还可以使用 object.key
表示法引用 map 中的值。例如,可以使用 @{map.key}
代替 map[key]
。
您可以使用单引号括起属性值,在表达式中使用双引号,如下面代码所示:
android:text='@{map["firstName"]}'
您也可以使用双引号括起属性值,在表达式中使用反引号(`),注意不是单引号:
android:text="@{map[`firstName`]}"
在表达式中,您可以使用如下语法访问资源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式字符串和复数可以通过参数进行计算:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当复数有多个参数时,应传递所有参数:
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
有些资源需要指定显示类型,如下表所示:
Type | Normal reference | Expression reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
Data Binding 允许您编写表达式来处理 View 分发的事件(例如,onClick()
方法)。事件的属性名由监听器(Listener)方法的名称确定,不过也存在一些例外。例如,View.OnClickListener
有一个 onClick()
方法,因此这个事件的属性是 android:onClick
。
对于点击事件,有一些特殊的事件处理程序需要一个不同于 android:onClick
的属性来避免冲突。您可以使用以下属性来避免这些类型的冲突:
类 | 监听器设置者(Listener setter) | 属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
您可以使用以下机制来处理事件:
null
,Data Binding 不会创建监听器,并将目标 View 的监听器设置为 null
。 事件可以直接绑定到处理方法,类似于可以将 android:onClick
分配给 Activity 中的一个方法。绑定表达式是在编译时处理,如果该方法不存在或者其签名不正确,则会出现编译错误。
在方法应用中,当数据被绑定时会创建实际的监听器实现。
若要将事件分配给它的处理程序,请使用普通的绑定表达式,该表达式的值就是要调用的方法名。例如:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
绑定表达式可以将 View 的点击监听器分配给 onClickFriend()
方法,如下所示:
<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>
注意:表达式中方法的签名必须与监听器对象中方法的签名完全匹配。即方法的参数必须与事件监听器的参数匹配,并且返回相同的类型(除非返回的是 void)。
监听器绑定是在事件发生时运行的绑定表达式。它们与方法引用类似,但是它们允许您运行任意的数据绑定表达式。这个特性在 Android Gradle 插件的 2.0 以及更高版本中支持。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只要您的返回值必须与监听器的返回值匹配(除非返回的是 void)。例如,考虑下面的拥有 onSaveClick()
方法的 presenter 类:
public class Presenter {
public void onSaveClick(Task task){}
}
然后,可以将点击事件绑定到 onSaveClick()
方法,如下所示:
<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>
当在表达式中使用回调时,Data Binding 会自动创建必要的监听器并为事件注册它。当 View 触发事件时,Data Binding 会计算给定的表达式。与常规绑定表达式一样,在计算监听器表达式时,将得到 null,并且是线程安全的。
在上面的例子中,我们没有定义传递给 onClick(View)
的 view
参数。监听器绑定为监听器参数提供了两种选择:忽略方法的所有参数,或命名所有参数。如果您想命名参数,可以在表达式中使用它们。例如,上面的表达式可以写成下面的形式:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者,如果您想在表达式中使用参数,则可以按照下面的方式工作:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
您可以使用带有多个参数的 lambda 表达式:
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
如果您正在监听的事件返回的值的类型不是 void
,那么您的表达式也必须返回相同类型的值。例如,如果您想监听长按事件,那么表达式应返回布尔值。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于 null
对象而无法计算表达式,那么 Data Binding 将返回该类型的默认值。例如,引用类型为 null
,int
为 0
,boolean
为 false
等等。
如果需要使用带判断的表达式(例如,三元表达式),您可以将 void
作为符号使用。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
监听器表达式功能非常强大,并且可以使您的代码易于阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像将可用数据从 UI 传递到回调方法一样简单。您应该在回调方法中实现业务逻辑。
Data Binding 库提供 Import、Variable 和 Include 等特性。Import 使得很容易在布局文件中引用类。Variable 允许您描述可用于 Binding 表达式的属性。Include 让您在整个 App 中重用复杂布局。
Import 允许您在布局文件中轻松引用类,就在代码中一样。在 data
元素中可以包含 0 个或多个 import
元素。下面的示例将 View
类导入布局文件:
<data>
<import type="android.view.View"/>
data>
当存在类名冲突时,可以为一个类设置别名。下面的示例为 com.example.real.estate.View
设置别名 Vista
:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
您可以使用 Vista
引用 com.example.real.estate.View
。
用作变量和表达式的类型引用
下面的示例展示了将 User
和 List
用作变量的类型:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<View>"/>
data>
进行类型强制转换
下面的示例将 connnection
属性强制转换为 User
类型:
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
引用导入类的 static 字段和方法
下面的示例如何引用 View
类的 VISIBLE
和 GONE
常量:
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
下面的示例引用了 MyStringUtils
类的 capitalize
方法:
<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"/>
可以在 data
元素中添加多个 variable
元素。每个 variable
元素描述一个可以在布局上设置的属性,可以在 Binding 表达式中使用这个 variable
。下面的示例描述了 user
、image
和 note
变量:
<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
接口的基类和接口,则不会观察这个变量。
当各种配置(例如,横屏和竖屏)使用不同布局文件时,Variable 就会被组合起来。因此在这些布局文件中不能出现变量命名冲突。
在生成的 Binding 类中,每个 Variable 都有 setter 和 getter 方法。在调用 setter 方法前,变量拥有默认值(例如,引用类型为 null
,int
类型为 0
,boolean
类型为 false
)。
将会自动生成一个名为 context
的特殊 Variable。这个 context
的值是通过根 View 的 getContext()
方法获取。如果显示声明了一个名为 context
的 Variable,将会覆盖自动生成的 context
变量。
可以将 Variable 传入 Include 的布局文件中。例如:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="testUser" 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="@{testUser}"/>
<include layout="@layout/contact"
bind:user="@{testUser}"/>
LinearLayout>
layout>
使用 bind:user="@{testUser}"
可以将变量 testUser
传递给 name.xml
和 contact.xml
布局文件中的变量 user
。
Data Binding 不支持 include 作为 merge 元素的直接子元素。例如,不支持以下布局:
<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>
可观察性是指一个对象在其数据发生变化时通知其他对象的能力。Data Binding 库允许您使对象(Object)、字段(Field)或集合(Collection)可观察。
任何 plain-old 对象(POJO)都可用于 Data Binding,但是修改对象不会自动更新 UI。Data Binding 能够让数据对象拥有当数据发生变化时通知其他对象的能力,即监听器(Listener)。有三种不同类型的可观察类:对象、字段和集合。
当可观察数据对象被绑定到 UI 后,如果这个数据对象的属性发生变化,UI 将自动更新。
提供泛型 ObservableField
类和一些使字段可观察的原始具体类:ObservableBoolean
、ObservableByte
、ObservableChar
、ObservableShort
、ObservableInt
、ObservableLong
、ObservableFloat
、ObservableDouble
和 ObservableParcelable
。
可观察字段是具有单个字段的自包含可观察对象。原始的版本在访问期间避免装箱和解箱。要使用此机制,请在 Java 编程语言中创建一个 public final
属性,如下面的例子所示:
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
使用 get()
和 get()
方法访问字段值,如下所示:
user.firstName.set("Google");
int age = user.age.get();
注意:在 Android Studio 3.1 以及更高版本中,可以使用 LiveData 对象替换可观察字段。
一些 App 使用动态结构来保存数据。可观察集合允许访问使用 Key 的这类结构。
当 Key 是引用类型(例如,String
)时,可以使用 ObservableArrayMap
。如下面的例子所示:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中,可以使用 String Key 找到 Map,如下所示:
<data>
<import type="android.databinding.ObservableMap"/>
<variable name="user" type="ObservableMap" />
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"/>
当 Key 是整数时,可以使用 ObservableArrayMap
。如下所示:
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局中,可以通过索引访问列表,如下所示:
<data>
<import type="android.databinding.ObservableList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableList/>
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
接口的类允许注册监听器,该监听器希望当可观察对象的属性变更时得到通知。
该 Observable
接口具备添加和移除监听器的机制,但您必须决定何时发送通知。为了简化开发,Data Binding 库提供了 BaseObservable
类,该类实现了监听器注册机制。实现 BaseObservable
的数据类负责在属性发生变化时发出通知。通过给 getter 方法添加一个 Bindable
注解和在 setter 方法中调用 notifyPropertyChanged()
来实现,如下面的例子所示:
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);
}
}
Data Binding 在模块(module)包中产生一个名为 BR
的类,该类包含了用于数据绑定的资源 ID。在编译期间,Bindable
注解会在 BR
类中会产生一个实体。
如果数据类的基类不能改变(即不能继承 BaseObservable),可以使用一个 PropertyChangeRegistry
对象进行注册和通知监听器,从而实现 Observable
接口。
如果想监听一个可观察的数据对象的数据变更,需要手动调用addOnPropertyChangedCallback()
和 removeOnPropertyChangedCallback()
。注意:如果布局文件中使用了可观察的数据对象,Data Binding 库会生成调用这两个函数的代码。
Data Binding 库会生成绑定(Binding)类,用于访问布局的变量(Variable)和 View。
生成的绑定类将布局中的变量和 View 链接起来。所有生成的绑定类都继承至 ViewDataBinding
类。这个绑定类拥有从布局属性(例如 user
变量)到布局中的 View 的所有绑定,并知道如何为绑定表达式赋值。
为每个布局文件生成一个绑定类。默认情况下,基于布局文件的名称给这个绑定类命名,采用“帕斯卡命名法”(即大驼峰命名法)并添加 Binding 后缀。该类放在 module 包的 databinding
包中。例如,布局文件 contact_item.xml
生成 ContactItemBinding
类。如果 module 包是 com.example.my.app
,则这个绑定类放在 com.example.my.app.databinding
包中。
可以通过调整 data
元素的 class
属性,来重命名绑定类或放在不同的包中。例如,下面的布局在当前 module 的 databinding
包中生成 ContactItem
绑定类:
<data class="ContactItem">
…
data>
您可以在不同的包中生成绑定类,方法是在类名前加一个句点(.
)。以下的例子在 module 包中生成 ContactItem
绑定类:
<data class=".ContactItem">
…
data>
以下例子在 com.example
包中生成 ContactItem
绑定类:
<data class="com.example.ContactItem">
…
data>
绑定对象应该在 inflate 布局后马上创建,。将对象绑定到布局的最常见方法是使用绑定类的 static 方法。您可以使用绑定类的 inflate()
方法 inflate view 层次并将对象绑定到此视图层次,如以下示例所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
}
inflate()
方法还可以接受一个 ViewGroup
对象,如以下示例所示:
MyLayoutBinding binding = MyLayoutBinding.inflate(
getLayoutInflater(), viewGroup, false);
如果使用不同的机制 inflate 布局,则可以将其单独绑定,如下所示:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时无法提前知道绑定类型。在这种情况下,可以使用 DataBindingUtil
类创建这个绑定,如下所示:
View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);
如果您在 Fragment
、ListView
和 RecyclerView
适配器中使用数据绑定项,您可能更喜欢使用绑定类的 inflate()
方法或 DataBindingUtil
类,如下所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(
layoutInflater, R.layout.list_item, viewGroup, false);
Data Binding 库在 Binding 类中为布局文件中拥有 ID 的每个 View 创建一个不可变字段。例如,Data Binding 库将为以下布局创建 TextView
类型的 firstName
和 lastName
字段:
<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()
更快。
与普通 View 不同,ViewStub
对象最开始是一个不可见的 View。当被设置为可见或者被显式调用 inflate () 后,它们会通过 inflate 其他布局来替换自己。
布局文件中的 ViewStub
,在对应的 Binding 类中生成对应的 ViewStubProxy
对象,而不是 ViewStub
对象。当 ViewStub
存在时,可以使用 ViewStubProxy
对象访问;当 ViewStub
已经 inflate 后,可以使用 ViewStubProxy
对象访问 inflate 的 View 层次结构。
当 inflate 另一个布局时,必须为新布局建立绑定。因此,ViewStubProxy
必须监听 ViewStub
的 OnInflateListener
,并在需要时建立绑定。ViewStubProxy
允许您设置一个 OnInflateListener
,但是由于同时只能存在一个 Listener,所以将在建立绑定后调用它(实际上就是先调用 ViewStubProxy
的 OnInflateListener
,然后再调用您自己设置的 OnInflateListener
)。
Data Binding 库为布局文件中的每个 Variable 生成访问器(accessor)方法。例如,下面的布局在 Binding 类中为 user
、image
和 note
变量生成 setter 和 getter 方法:
<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>
当一个变量或可观察对象发生变化时,绑定将在下一帧前执行。如果我们需要立即执行绑定,可以调用 executePendingBindings()
,必须在 UI 线程中调用它。
有时,不知道具体的绑定类。例如,一个 RecyclerView.Adapter
不知道具体的绑定类。但是,在调用 onBindViewHolder()
方法时仍然需要使用绑定值。
在以下示例中,RecyclerView
绑定的所有布局中有一个 item
变量。BindingHolder
对象有一个 getBinding()
方法,此方法返回一个 ViewDataBinding
基类。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = items.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
注意:Data Binding 库在 module 包中生成一个名为 BR 的类,该类包含了用于数据绑定的资源 ID。在上面的示例中,库自动生成这个
BR.item
变量。
您可以在后台线程中更改数据模型,只要它不是一个集合。数据绑定在计算期间对每个变量/字段进行本地化,以避免任何并发问题。
绑定适配器(Binding Adapter)负责通过适当的框架调用来设置值。比如通过 setText()
方法设置属性值;通过 setOnClickListener()
方法设置事件 Listener。
Data Binding 库允许您通过适配器指定设置值的方法(实现您自己的绑定逻辑),以及指定返回对象的类型。
当绑定变量值发生变化时,生成的绑定类会调用对应的 setter 方法。您可以让 Data Binding 库自动确定方法,或者显式声明方法,或者通过自定义逻辑选择方法。
对于一个 example
属性,Data Binding 库会自动尝试查找参数类型兼容的 setExample(arg)
方法。在搜索这个方式时,不需要考虑属性的命名空间,仅仅只需要考虑属性名和参数类型。
即使给定的属性名不存在,Data Binding 仍可以工作。您可以通过 Data Binding 为任何 setter 方法创建属性。例如,支持类 DrawerLayout
没有任何属性,但是有很多 setter 方法。下面的布局自动使用 setScrimColor(int)
和 setDrawerListener(DrawerListener)
方法作为属性 app:scrimColor
和 app:drawerListener
的 setter 方法:(有对应的 setter 方法,但是没有对应的属性,可以给其一个自定义属性)
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
某些属性的 setter 方法名称不匹配。在这种情况下,可以使用 BindingMethods
注解将属性与 setter 方法进行关联。这个注解是用于类,可以包含多个 BindingMethod
注解。在下面的示例中,属性 android:tint
与 setImageTintList(ColorStateList)
方法关联:(有对应的 setter 方法,并且有对应的属性,将它们关联)
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
public class TestBindingAdapter {
}
通常情况下,您不需要为 Android Framework 中的类重命名 setter 方法。
某些属性需要自定义绑定逻辑。例如,属性 android:paddingLeft
没有与其相关联的 setter 方法。带有 BindingAdapter
注解的 static 绑定适配器方法允许您自定义如何调用属性的 setter 方法。(有对应的属性,但是没有对应的 setter 方法,为其提供一个自定义方法)
例如,以下示例显式了 paddingLeft
属性的绑定适配器:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
第一个参数的类型和与属性关联的 View 类型相同;第二个参数的类型和属性的绑定表达式返回的类型相同。
自定义的绑定适配器将重载 Android Framework 提供的。
您还可以使用接收多个属性的适配器,如以下示例所示:
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}
您可以在布局中使用这个适配器,如以下示例所示:(注意:@drawable/venueError
引用 App 中的一个资源。使用 @{}
包围该资源使其成为一个有效的绑定表达式。)
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />
注意:数据绑定库会忽略自定义命名空间进行匹配。
如果 imageUrl
和 error
都用于 ImageView
对象,并且 imageUrl
是字符串和 error
是 Drawable
时,将调用这个适配器。如果您希望在设置任何一个属性时都调用适配器,可以将这个适配器的 requireAll
标志设置为 false
,如以下示例所示:
@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);
}
}
绑定适配器方法可以选择在其处理程序中使用旧值。采用旧值和新值的方法应该首先声明属性的所有旧值,然后声明新值,如以下示例所示:
@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());
}
}
事件处理程序只能与带有一个抽象方法的接口或抽象类一起使用,如以下示例所示:
@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 android:onLayoutChange="@{() -> handler.layoutChanged()}"/>
当一个监听器有多个方法时,必须将其拆分为多个监听器。例如,View.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow(View)
和 onViewDetachedFromWindow(View)
。库提供了两个接口来区分属性和处理程序:
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为更改一个监听器也会影响另一个监听器,所以您需要一个适用于任一属性或适用于两者的适配器。您可以在注解中将 requireAll
设置为 false
,以指定不是每个属性都必须分配绑定表达式,如以下示例所示:
@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) {
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 oldListener = ListenerUtil.trackListener(
view, newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子比正常情况稍微复杂一些,因为 View 使用 addOnAttachStateChangeListener()
和 removeOnAttachStateChangeListener()
方法代替 OnAttachStateChangeListener
的 setter 方法。android.databinding.adapters.ListenerUtil
类帮助跟踪以前的监听器,以便在绑定适配器中删除它们。
通过 @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解 OnViewDetachedFromWindow
和 OnViewAttachedToWindow
,Data Binding 代码生成器知道仅当在 Android 3.1 (API 12) 以及更高版本上运行时才生成此监听器。
当从绑定表达式返回对象时,Data Binding 库会选择用于设置属性值的方法。这个对象将会被强制转换为选择方法的参数类型。当使用 ObservableMap
类存储数据时,这是非常有用的,如下面的例子所示:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意:您也可以使用 object.key 表示法引用 map 中的值。例如,可以使用
@{userMap.lastName}
代替userMap["lastName"]
。
在表达式中的 userMap
对象返回一个值,该值将自动转换为 setText(CharSequence)
方法的参数类型,此方法用于设置 android:text
属性的值。如果参数类型不明确,您必须在表达式中强制转换返回类型。
在某些情况下,需要在特定类型之间进行自定义转换。例如 View 的 android:background
属性需要一个 Drawable,但是指定的 color
值是一个整型。
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
可以通过一个用 BindingConversion
注解的 static 方法将 int
类型转换为 ColorDrawable
类型:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
但是,表达式中的类型必须是一致的,即在同一个表达式中不能使用不同的类型,如下面的例子所示:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Data Bingding 库能够与架构组件无缝协作,从而进一步简化 UI 的开发。App 中的布局可以绑定到架构组件中的数据,这些数据帮助您管理 UI 控制器的生命周期并通知数据的变更。
您可以使用 LiveData
对象作为数据绑定源,自动通知 UI 数据发生变更。
与实现 Observable
的对象(如可观察字段)不同,LiveData 对象了解订阅数据变更观察者的生命周期。在 Android Studio 3.1 及更高版本中,在数据绑定代码中可以使用 LiveData 对象替换可观察字段。
要想一起使用 LiveData
和 绑定类,需要指定一个生命周期所有者来定义 LiveData 对象的范围。下面的例子在实例化绑定类后将 Activity 指定为声明周期所有者:
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
组件将数据绑定到布局。在 ViewModel
组件中,可以使用 LiveData
对象转换数据或合并多个数据源。下面的例子展示了如何在 ViewModel
中转换数据:
class ScheduleViewModel extends ViewModel {
LiveData username;
public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}
}
Data Binding 库能够与 ViewModel
组件无缝协作,ViewModel
组件向外提供数据。 一起使用 Data Binding 库和 ViewModel
组件,可以将 UI 逻辑从布局移动到组件中,这样将使得更易于测试。Data Binding 库确保在需要时将 View 与 数据源进行绑定和解绑。剩下的大部分工作用于确保公开正确的数据。
一起使用 Data Binding 库和 ViewModel
组件的步骤:
ViewModel
类的组件。ViewModel
组件赋给绑定类的一个属性。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;
}
}
在布局中,使用绑定表达式将 ViewModel
组件的属性和方法分配给相应的 View,如下面的示例所示:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
您可以使用实现 Observable
的 ViewModel
组件来通知其他 App 组件数据发生变更,类似于使用 LiveData
对象。
在某些情况下,您可能更喜欢使用实现了 Observable
接口的 ViewModel
组件,而不是 LiveData
对象。使用实现了 Observable
的 ViewModel
组件,可以更好地控制 App 中的绑定适配器。例如,此模式使您在数据发生变更时更好地控制通知,还允许您在双向绑定中指定设置属性值的自定义方法。
要实现可观察的 ViewModel
组件,必须创建一个继承至 ViewModel
并实现 Observable
接口的类。当观察者使用 addOnPropertyChangedCallback()
和 removeOnPropertyChangedCallback()
订阅或取消订阅通知时,您可以提供自定义逻辑。当属性发生变化时,您也可以在 notifyPropertyChanged()
中提供自定义逻辑。下面的代码展示了如何实现一个可观察的 ViewModel
:
/**
* 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);
}
}
使用单向数据绑定,您可以给一个属性设置一个值,并且设置一个监听器来响应此属性的变更。
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}" />
双向绑定提供了一种快捷方式:
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}" />
@={}
符号同时接收属性的数据变更和监听用户更新。
如果您想使用自定义属性的双向数据绑定,您需要使用 @InverseBindingAdapter
和 @InverseBindingMethod
注解。
例如,如果您想在名为 MyView
的自定义View 中对 time
属性启用双向数据绑定,请完成以下步骤:
使用 @BindingAdapter
注解 setter 方法:
@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}
使用 @InverseBindingAdapter
注解 getter 方法:
@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
return view.getTime();
}
此时,Data Binding 知道当数据变更时要做什么(调用被 @BindingAdapter
注解的方法),以及当 View 属性变更时要做什么(调用被 @InverseBindingAdapter
注解的方法)。但是,它不知道属性何时或如何更变。
为此,您需要在 View 上设置监听器。它可以是与自定义 View 关联的自定义监听器,也可以是通用事件,例如焦点丢失或文本变更。给一个方法添加 @BindingAdapter
注解,该方法用来给此属性变更设置监听器:
@BindingAdapter("app:timeAttrChanged")
public static void setListeners(
MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
}
该监听器有一个 InverseBindingListener
类型的参数。您可以使用这个 InverseBindingListener
告知 Data Binding 系统此属性发生变更。然后,系统将调用被 @InverseBindingAdapter
注解的方法,等等。
注意:每个双向绑定会生成一个合成事件属性。此属性的名称与基本属性相同,但它会添加后缀“AttrChanged”。合成事件属性允许库创建一个使用
@BindingAdapter
注解的方法,该方法将事件监听器关联到适当的 View 实例。
在实践中,这个监听器包含一些重要的逻辑,包括用于单向数据绑定的监听器。有关示例,请参阅文本属性变更的适配器 TextViewBindingAdapter
。
如果绑定到 View 的变量需要在显示之前以某种方式进行格式化,转换或更改,则可以使用 Converter
对象。
例如,一个显示日期的 EditText
对象:
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}" />
这个 viewmodel.birthDate
属性包含一个 Long
类型的值,因此需要使用转换器对其进行格式化。
因为使用的是双向表达式,所以还需要一个反转转换器,让库知道如何将用户提供的字符串转换回支持的数据类型(在本例中是 Long
)。需要给其中的一个转换器添加 @InverseMethod
注解。
public class Converter {
@InverseMethod("stringToDate")
public static String dateToString(EditText view, long oldValue,
long value) {
// Converts long to String.
}
public static long stringToDate(EditText view, String oldValue,
String value) {
// Converts String to long.
}
}
在使用双向绑定时,需要注意不要引入无限循环。当用户更改属性时,将调用被 @InverseBindingAdapter
注解的方法,并将该值分配给对应属性。反过来,这将调用被 @BindingAdapter
注解的方法,这将导致调用被 @InverseBindingAdapter
注解的方法,以此类推。
因此,需要在被 @BindingAdapter
注解的方法中比较新值和旧值来避免陷入无限循环。
平台已经对下表中的属性提供了双向数据绑定支持。关于平台如何提供这种支持的详细信息,请参阅相应绑定适配器的实现:
类 | 属性 | 绑定适配器 |
---|---|---|
AdapterView | android:selectedItemPosition android:selection |
AdapterViewBindingAdapter |
CalendarView | android:date | CalendarViewBindingAdapter |
CompoundButton | android:checked | CompoundButtonBindingAdapter |
DatePicker | android:year android:month android:day |
DatePickerBindingAdapter |
NumberPicker | android:value | NumberPickerBindingAdapter |
RadioButton | android:checkedButton | RadioGroupBindingAdapter |
RatingBar | android:rating | RatingBarBindingAdapter |
SeekBar | android:progress | SeekBarBindingAdapter |
TabHost | android:currentTab | TabHostBindingAdapter |
TextView | android:text | TextViewBindingAdapter |
TimePicker | android:hour android:minute |
TimePickerBindingAdapter |
下面列出 Observable 与 LiveData 的简单对比(如果您不了解 LiveData,可以参阅 这里):