Data-Binding 是一种支持库,借助该库,我们可以将布局中的界面和应用中的数据进行绑定。
通常我们要为一个控件赋值:
TextView textView = findViewById(R.id.sample_text);
textView.setText(viewModel.getUserName());
而使用 Data-Binding 则可以直接在布局文件中将文本直接分配到控件中,这样就无须上面的代码:
<TextView
android:text="@{viewmodel.userName}" />
借助布局中间中的绑定组件,我们就可以移除 Activity 中的许多界面框架的调用,使得维护起来更简单、方便。还可以提高应用性能,并且有助于防止内存泄漏以及避免空指针异常。
要想使用数据绑定,需要保证 Android API 4.0(API14)或以上版本,并且 Gradle 版本在 1.5.0 或者以上。
还需要在项目中开启数据绑定功能:
android {
...
dataBinding {
enabled = true
}
}
Gradle 3.1.0-alpha06 包含一个新的数据绑定编译器,这个新的编译器会逐步创建绑定类,一般情况下会加快构建过程,要启用这个新编译器也很简单,编辑 gradle.properties
文件,添加以下内容:
android.databinding.enableV2=true
你还可以通过添加下面的参数在 gradle 命令中启用新编译器:
-Pandroid.databinding.enableV2=true
Gradle 插件 3.1 版中的新数据绑定编译器不向后兼容。你需要生成所有绑定类,并启用该功能以利用增量编译。但是 3.2 中的新编译器与先前版本生成的绑定类兼容。默认情况下启用 3.2 版本中的新编译器。
表达式语言允许您编写处理视图调度的事件的表达式。数据库绑定自动生成将布局中的视图与对象绑定所需的工具类。
数据绑定布局文件是以 layout
标签开头,后面跟 data
元素和 View
的根元素,这个 View 根元素
就是我们平时写的布局文件,下面是一个简单的例子:
<?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
变量描述了一个在这个布局中使用的属性:
<variable name="user" type="com.example.User" />
布局中使用 @{}
来设置属性,这里的 TextView
文本设置为 User 变量的 firstName 属性值。
假设有一个普通的对象来描述 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;
}
}
上面两种写法从数据绑定的角度来看是等价的,也就是说,对象的属性要不可以直接访问,要不提供访问接口。
系统会自动为我们的绑定布局生成绑定类,规则是布局文件名+Binding
,例如上面的布局文件名是:activity_main.xml
,那么自动生成的绑定类就是 ActivityMainBinding
。该类包含布局属性(例如 user)到布局视图的所有绑定,并知道如何为绑定表达式指定值。创建绑定的推荐方法是在扩展布局时执行此操作,如下例所示:
@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);
}
在运行时,App 就可以在 UI 中显示 Test 用户。你还可以使用 LayoutInflater
获取视图,就像这样:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果您在 ListView
或 RecyclerView
Adapter 内使用数据绑定项目,则可能更愿意使用:
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}'
有些你在Java中能使用的表达式语法在这里会缺少一些操作符。
this
super
new
null 合并运算符 (??)
:左边如果不为空,则选择左边,如果左边为空,则选择右边。
android:text="@{user.displayName ?? user.lastName}"1
这在功能上等同于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
就跟刚开始的例子一样,对于 JavaBean 的引用,当一个表达式引用一个类的属性时,不管属性是直接可以访问还是提供了 setter/getter
方法,都是直接使用属性来标识,对于 setter/getter
方法,系统会自动解析。
android:text="@{user.lastName}"
生成的数据绑定代码自动检查空值并避免空指针异常。例如,在表达式中 @{user.name}
,如果 user
为 null
,user.name
将被赋予其默认值(null
)。如果你是引用 user.age
,年龄是一个 int
,那么它将默认为0
。
常见的集合:数组,列表,稀疏列表(sparse lists),和映射集合(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]}"
注意,在语句中如果有
<
或者>
,必须对它们进行转义,<
转义为<
,>
转义为>
。还可以使用
map.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)}"
当一个复数有多个参数时,应该传递所有的参数:
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 Binding 可以通过写表达式来处理 View 分发的事件,例如 onClick()
方法。事件属性名由 listener 方法的名字确定,但存在个别例外。例如 View.OnClickListener
有个方法 onClick()
,这时事件的属性名是android:onClick
。
click 事件有一些专门的事件处理程序需要使用除了 android:onClick
之外的属性来描述以避免冲突,可以使用以下属性来避免这些类型冲突:
Class | Listener setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
您可以使用以下机制来处理事件:
引用方法Method Reference
在表达式里,可以引用符合监听器签名的方法。当表达式的值是方法引用时,DataBinding 将方法引用和所有者对象包装在监听器中,并在目标 View 上设置该监听器。如果表达式的值为 null,则 DataBinding 不会创建监听器并设置空监听器。
绑定监听器Listener Bindings
这些是在事件发生时计算的 lambda 表达式。DataBinding 会创建一个监听器并设置在 View 上。在发生事件时,监听器将会计算 lambda 表达式。
事件可以和处理方法直接绑定,类似于 android:onClick
的方法可以分配给 Activity 中的方法一样。和 View 中的 onClick 属性相比,其最主要的优点是表达式在编译的时候就会处理,因此如果该方法不存在或者签名不正确,在编译时就会发现。
引用方法和绑定监听器之间的区别主要在于引用方法的实际的监听器实现是在绑定数据时创建的,而不是事件触发时创建的。如果你希望在事件发生时计算表达式,请使用绑定监听器。
要将事件分配给其处理程序,请使用普通绑定表达式,其值为要调用的方法名称。 例如:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
表达式可以将 View 的点击事件监听器分配给 onClickFriend
方法,如下所示:
<?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>
注意:表达式中方法的签名必须与监听器对象中方法的签名完全匹配。
绑定监听器是在事件发生时运行的绑定表达式。它和引用方法类似,但他们可以运行任务数据绑定表达式,该功能在 Gradle 2.0 及以上版本可用。
在引用方法中,方法的参数必须和事件监听器的参数匹配。在绑定监听器中,只要求返回值必须和监听器的预期返回值匹配,除非监听器返回空。例如:
public class Presenter {
public void onSaveClick(Task task){}
}
然后你可以将 click 事件绑定到 onSaveClick()
方法,例如:
<?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>
在表达式中使用回调的时候,DataBinding 会自动创建必要的监听器并为事件注册该监听器。当 View 触发事件时,DataBinding 会计算给定的表达式。和常规绑定表达式一样,在计算这些监听器表达式时,你仍然可以获得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
的值,则您的表达式必须返回相同类型的值。例如,如果要监听长按事件,则表达式应该返回 boolean
。
public class Presenter {
public boolean onLongClick(View view, Task task) { }
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
如果由于null对象导致无法计算表达式,数据绑定将返回该类型的默认 Java 值。例如,null
用于引用类型,0
用于 int
类型, false
用于 boolean
类型等。
如果您需要使用谓词(例如三元)表达式,则可以将void
用作符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听器
监听器表达式非常强大,可以让您的代码非常容易阅读。另一方面,包含复杂表达式的监听器会使您的布局难以阅读和维护。这些表达式应该像从UI中传递可用数据到回调方法一样简单。您应该在您从监听器表达式调用的回调方法内实现任意的业务逻辑。
DataBinding 提供诸如 import、variables 和 includes 的功能。import 会让布局中的类很容易引用。variables 可以描述用于绑定表达式的属性。include 可以在应用中重复利用布局。
import
元素中可以使用零个或多个 data
元素。这些就像在 Java 中一样可以轻松地引用布局文件中的类。
<data>
<import type="android.view.View"/>
</data>
import View 类可以使得我们在绑定表达式中使用它。以下示例展示了如何引用 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}"/>
当 import 的类名发生冲突时,可以为其中一个类设置别名。下面的例子将 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
,View 可以用来引用布局文件中的android.view.View
。
import 的类型可以作为变量和表达式中的类型引用。下面的例子展示了用作变量类型的 User 和 List:
<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问题。
你还可以使用 import 的类型来对表达式进行强制转换,下面的例子展示了将 cconnection 强制转换为 User 类型:
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在表达式中引入静态字段和方法时,也可以使用 import,下面的代码展示了 import 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"/>
就像在 Java 中一样,java.lang.*
会自动导入包下所有类。
你可以在 data
元素中使用多个 variable
。每个 variable
描述可以在布局上设置的属性,可以在布局文件的绑定表达式中使用:
<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集合,那么这个类型应该被描述出来。如果变量没有实现 Observable 接口的基类或接口,变量将不会被检查!
当不同的配置文件(例如横向或纵向)有不同的布局文件时,变量将被合并。这些布局文件之间不得存在冲突的变量定义。
生成的绑定类将为每个描述的变量设置一个setter
和getter
。变量将采用默认的Java值,直到setter
被调用 - null
用于引用类型,0
用于int
类型, false
用于boolean
类型等。
根据需要生成一个名为context
的特殊变量用于绑定表达式。context
值来自根视图的getContext()
得到的Context
。该context
变量将被具有该名称的显式变量声明覆盖。
通过在属性中使用应用程序命名空间和变量名称,变量可以从包含的布局传递到容器的布局的绑定中:
<?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
元素的直接子元素。例如, 不支持以下布局:
<?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>
可观察性是只对象通知其他人数据变化的能力。DataBinding 允许您让对象、字段或者集合变得可观察。
任何普通对象都可以用于数据绑定,但修改对象不会自动更新 UI。DataBinding 可以在数据对象数据更改时通知其他对象。有三种不同类型的可观察类:对象、字段和集合。
当其中一个可观察对象绑定到 UI 并且数据对象的属性发生更改时,UI 将自动更新。
如果你的类只有几个属性,那么可以通过使用通用 Observable 类和以下基于原语的类来使得字段变为可观察:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
ObservableFields
是具有单个字段的独立 observable 对象。原始版本在访问操作期间避免装箱和取消装箱。要使用,请在数据类中创建一个公共 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();
}
要访问字段值,请使用 set()
和 get()
访问器方法:
user.firstName.set("Google");
int age = user.age.get();
注意:Android Studio 3.1 及更高版本允许您使用 LiveData 对象替换可观察字段,这为您的应用提供了额外的好处。
一些应用程序使用更加动态化的结构来保存数据。Observable 集合允许对这些数据对象进行键存取。当键是 String 等引用类型时 ObservableArrayMap
非常有用:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局中,可以通过 String 键访问 map:
<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<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<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 接口的类允许注册一个监听器,当可观察对象的属性更改时通知这个监听器。
Observable 接口具有添加和删除监听器的机制,但是我们必须决定何时发送通知。为了使开发更简单, DataBinding 库提供了 BaseObservable 类(此类实现了 Observable 接口),它实现了监听器注册机制。继承了 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);
}
}
数据绑定在模块包中会生成一个名为BR
的类,其中包含了用于数据绑定的资源ID,Bindable 注解在编译期间会在 BR
类中生成一个条目。如果你无法更改数据类的基类,则可以使用 PropertyChangeRegistry 对象实现 Observable 接口,以便有效地注册和通知监听器。
DataBinding 库会帮我们生成用于访问布局中的变量和 View 的 Binding 类,这一节将说明如何创建和自定义生成的 Binding 类。
DataBinding 库会为每个布局文件生成一个 binding 类,生成的 binding 类将布局中的 View 与布局变量链接起来,并且我们可以自定义 binding 类的名称和包,另外所有生成的 binding 类都继承自 ViewDataBinding 。
默认情况下,该类的名称基于布局文件的名称,将布局名称转换为 Pascal 格式并向其添加 Binding 后缀。例如布局文件名是 activity_main.xml
,相应的生成 MainActivityBinding
类 。这个类持有了布局属性(例如user变量)到布局 View 的所有绑定,并知道如何为绑定表达式分配值。
binding 对象应该在 inflat 布局后立即创建,以确保 View 层次在绑定到布局中的表达式视图之前不被修改。将对象绑定到布局的最常见方法是使用绑定类的静态方法,我们可以先将视图层次 inflate ,然后使用 binding 类的inflate()
方法,,膨胀视图层次结构并将其绑定到该层次结构,如下例所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
}
还有另一个版本 inflate() 方法,它除了 LayoutInflater 对象之外还需要一个 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 的 Adapter 中使用数据绑定 Item,我们可能更偏向于使用绑定类的 inflate() 方法 或 DataBindingUtil 类,如下面的代码所示:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
DataBinding库会在binding类中为布局中每个具有ID的View创建一个不可变字段。例如,DataBinding 库从以下布局创建 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>
DataBinding 库一次性从 View 层次结构中提取包含 ID 的 View,此机制要比调用 findViewById() 方法访问 View 更快。
ID 在数据绑定中并不是必须的,但有些情况下仍然需要在代码中访问 View。
Databinding 库会为布局中声明的每个变量生成访问器方法。例如,下面布局的 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>
与普通 View 不同,ViewStub 对象以不可见 View 开始,当它变得可见时,或被明确告知 inflate 时,他们通过 inflate 另一个布局来替换自身。
由于 ViewStub 实质上从 View 层次结构中消失了,binding 对象中的 View 也必须消失以便垃圾回收。因为这些 View 是 final 的,所以在生成的绑定类中一个 ViewStubProxy 对象会替代 ViewStub,让我们可以在 ViewStub 存在的情况下访问它,并在 ViewStub 已经 inflate 时访问 inflated 的 View 层次结构。
当 inflating 另一个布局时,必须为新布局建立绑定,因此,ViewStubProxy 必须要监听 ViewStub 的 OnInflateListener 并在必要时建立绑定。由于在给定时间只有一个监听器可以存在,所以 ViewStubProxy 允许我们设置一个 OnInflateListener,它在建立绑定后调用。
当一个变量或可观察对象发生更改时,绑定会安排在下一帧更改之前。然而,有时候,绑定必须立即执行,要想强制执行,请使用 executePendingBindings()
方法。
有时候,特定的绑定类是未知的。例如,RecyclerView.Adapter 针对任意布局进行操作,所以不知道特定的 binding 类,但它仍然必须在调用 onBindViewHolder() 方法期间分配 binding 值。
在下面示例中,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();
}
注意:DataBinding 库会在模块包中生成一个名为 BR 的类,其中包含用于数据绑定的资源的ID。在上面的例子中,DataBinding 库自动生成了 BR.item 变量。
我们可以在后台线程中更改数据模型,只要它不是集合。 DataBinding 在计算期间会本地化每个变量或字段以避免任何并发问题。
在默认情况下,绑定类根据布局文件的名称生成,以大写字母开头,删除下划线 (_)
,大写接下来的字母,并加上后缀 Binding。该类放在 databinding 模块包下的包中。例如,布局文件 contact_item.xml
生成 ContactItemBinding 类。如果布局所在模块包是 com.example.my.app
,则 binding 类会被放置在 com.example.my.app.databinding 包中。
我们可以通过调整 data 元素的 class 属性,将 binding 类进行重命名或放置在不同的包中。例如,以下布局会生成 ContactItem binding 类,位于当前模块的 databinding 包中:
<data class="ContactItem">
…
</data>
我们可以通过在类名前添加一个句点来在不同的包中生成binding类。以下示例在模块包中生成binding类:
<data class=".ContactItem">
…
</data>
我们也可以在要生成 binding 类的位置使用完整的包名称。以下示例在 com.example
包中创建 ContactItem
绑定类 :
<data class="com.example.ContactItem">
…
</data>
BindingAdapter 负责对适当的框架调用设置值。例如设置一个属性值,调用 setText()
方法,另一个例子是设置一个事件监听器,比如调用 setOnClickListener()
方法。
DataBinding 库允许我们指定调用的方法来设置值,提供我们自己的绑定逻辑,并通过使用适配器指定返回的对象的类型。
每当绑定值发生变化时,生成的绑定类必须使用绑定表达式在视图上调用 setter 方法,我们可以允许 DataBinding 库自动确定方法,显式声明方法或提供自定义逻辑来选择方法。
对于名为 example 的属性,DataBinding 库会自动尝试查找接受兼容类型作为参数的 setExample(arg) 方法,不考虑属性的命名空间,只有在搜索方法时使用属性的名称和类型。
例如,给定 android:text="@{user.name}"
表达式,DataBinding 库将查找接受 user.getName()
返回的类型的 setText(arg)
方法;如果 user.getName()
的返回类型是 String,则库将查找接受 String 参数的 setText()
方法;如果该表达式返回一个 int,则库将搜索接受 int 参数的 setText()
方法,表达式必须返回正确的类型,我们也可以根据需要强制返回值。
即使给定名称不存在任何属性,DataBinding 也可以工作。我们可以使用 DataBinding 为任何 setter 创建属性。例如,DrawerLayout 没有任何属性,但有很多 setter,以下布局将自动使用 setScrimColor(int)
和setDrawerListener(DrawerListener)
方法作为 app:scrimColor
和 app:drawerListener
属性,分别为:
<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 相关联,注解可以被应用到类上,并且可以包含多个 BindingMethods 注解,每个重命名的方法一个注解,在我们应用中的任何类都可以添加 BindingMethods 注解。在下面示例中,android:tint
属性与 setImageTintList(ColorStateList)
方法关联,而不与 setTint()
方法关联:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
大多数情况下,我们不需要重命名 Android 框架类中的 setter,这些属性已经实现了使用名称约定来自动查找匹配方法。
一些属性需要自定义绑定逻辑,例如,android:paddingLeft
属性没有关联的 setter,但是提供了 setPadding(left, top, right, bottom)
方法。使用 BindingAdapter 注解的静态绑定适配器方法允许我们自定义如何调用属性的 setter。
Android 框架类的属性已经创建了 BindingAdapter 注解,例如,以下示例显示 paddingLeft
属性的绑定适配器:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
绑定适配器方法的参数类型很重要,第一个参数确定与属性关联的 View 类型,第二个参数确定给定属性的绑定表达式中接受的参数类型。
绑定适配器对自定义其他类型很有用,例如,可以从工作线程调用自定义加载器来加载图像。
我们定义的绑定适配器会在发生冲突时覆盖 Android 框架提供的默认适配器。
我们还可以让适配器接收多个属性,如下所示:
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
我们还可以在布局中使用适配器,如下例所示:
注意,@drawable/venueError
是指应用中的资源,用 @{}
括住资源使其成为有效的绑定表达式。
<ImageView
app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}" />
注意:DataBinding 库会忽略用于匹配目的的自定义命名空间。
如果 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
类有助于跟踪以前的监听器,它们可能会在绑定适配器中被删除。
当从绑定表达式返回 Object 时,DataBinding 库选择用于设置属性值的方法,该 Object 被转换为所选方法的参数类型,使用 ObservableMap 类存储数据的应用中,此行为很方便,如下例所示:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
注意:我们也可以使用 object.key
表示法引用 map 中的值。例如,上述示例中的 @{userMap["lastName"]}
可以替换为 @{userMap.lastName}
。
表达式中的 userMap 对象返回一个值,该值自动转换为用于设置 android:text
属性值的 setText(CharSequence)
方法中找到的参数类型,如果参数类型不明确,则必须在表达式中转换返回类型。
在某些情况下,特定类型之间需要自定义转换。例如,View 的 android:background
属性需要 Drawable,但指定的颜色值是一个整数。以下示例显示了一个需要 Drawable 的属性,但是提供了一个整数:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
每当需要一个 Drawable 并返回一个整数时,该 int 应该转换为一个 ColorDrawable,转换可以使用带有 BindingConversion 注解的静态方法完成,如下所示:
@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"/>
在 AndroidX 这个库中包含了 架构组件,我们可以使用架构组件来设计更强大、可测试和可维护的应用程序。DataBinding 库可以与架构组件无缝协作,进一步简化 UI 的开发,应用程序中的布局可以绑定到架构组件的数据中,这些数据已经帮助我们管理了 UI 控制器(Activity 和 Fragment)的生命周期并可以在数据变更时通知 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 管理与 UI 相关的数据 章节所述)将数据绑定到布局。在 ViewModel 组件中,我们可以使用 LiveData 对象来转换数据或合并多个数据源。以下示例显示如何转换 ViewModel 中的数据:
class ScheduleViewModel extends ViewModel {
LiveData username;
public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}
DataBinding 库可以与 ViewModel 组件无缝协作,这样能够公开布局观察到的数据并对其变化做出响应。将 ViewModel 组件与 DataBinding 库配合使用,我们可以将 UI 逻辑移出布局并放入易于测试的组件中。DataBinding 库确保 View 在需要时从 数据源 绑定和解除绑定,剩下的大部分工作就是确保你公开正确的数据。
要将 ViewModel 组件与DataBinding 库一起使用,我们必须实例化我们的 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 组件来通知其他应用组件有关数据的更改,类似于使用 LiveData 对象。
在有些情况下,即使失去了 LiveData 的生命周期管理功能,我们也更愿意使用实现了 Observable 接口的 ViewModel 组件而不是使用 LiveData 对象。使用实现 Observable 的 ViewModel 组件可以更好地控制应用中的绑定适配器。例如,这种模式可以让我们在数据更改时更好地控制通知,还可以指定自定义方法来设置双向数据绑定中的属性值。
要实现可观察的 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}"
/>
@={}(主要包括“=”符号)
表示接收属性的数据更改并同时监听用户更新。
为了对支持数据的更改做出反应,您可以使布局变量成为 Observable 的实现,通常是 BaseObservable,并使用 @Bindable
注释,如以下代码片段所示:
public class LoginViewModel extends BaseObservable {
// private Model data = ...
@Bindable
public Boolean getRememberMe() {
return data.rememberMe;
}
public void setRememberMe(Boolean value) {
// 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);
}
}
}
由于 bindable 属性的 getter 方法称为 getRememberMe()
,因此属性的相应 setter 方法会自动使用名称 setRememberMe()
。
有关使用 BaseObservable 和 @Bindable
的更多信息,请参阅Work with observable data objects
平台提供了 最常用的双向绑定属性 双向绑定实现和监听器改变,你可以作为你 APP 的一部分使用。如果你想使用自定义属性的双向绑定,你需要使用 @InverseBindingAdapter
和 @InverseBindingMethod
注解。
举个例子,如果你想在名为 MyView
的自定义 view 上面开启 "time"
属性的双向绑定,通过以下几步完成:
在设置初始值的方法上添加注解,并在值使用 @BindingAdapter
更改时进行更新:
@BindingAdapter("time")
@JvmStatic fun setTime(view: MyView, newValue: Time) {
// 这里很重要,打破可能出现的死循环
if (view.time != newValue) {
view.time = newValue
}
}
在读取 View 值的方法上添加 @InverseBindingAdapter
注解:
@InverseBindingAdapter("time")
@JvmStatic fun getTime(view: MyView) : Time {
return view.getTime()
}
这个时候,Data Binding 知道在数据更改时要做什么(它会调用加了 @BindingAdapter
注解的方法)和在 View的属性更改时需要做什么(它会调用加了InverseBindingListener
注解的方法)。但是,它不知道属性何时更改或如何被更改。
所以,你需要在 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
来告诉 DataBinding 系统属性被修改了。系统会开始调用加了 @InverseBindingAdapter
注解的方法,等等。
每一个双向绑定生成了一个合成的事件属性。这个属性与基属性具有一个相同的名字,但是它包含了一个 “AttrChanged” 的后缀。合成事件属性允许库去创建一个使用
@BindingAdapter
注解的方法,以便将事件监听器与 View 的相应的实例相关联
在实践中,这个监听器中包含了一些不平凡的逻辑,包括单向数据绑定的监听器。举例见 text attribute change 的 adapter,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
注释的方法,并将值分配给 backing 属性。反过来,这将调用使用 @BindingAdapter
注释的方法,这将触发使用 @InverseBindingAdapter
注释的方法的另一个调用,以此类推。
因此,通过比较使用 @BindingAdapter
注释的方法中的新旧值来打破可能的死循环非常重要。
当您使用下表中的属性时,该平台为双向数据绑定提供内置支持。 有关平台如何提供此支持的详细信息,请参阅相应绑定适配器的实现:
Class | Attribute(s) | Binding adapter |
---|---|---|
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 |