Offical Tutorial
android
DataBinding
tutorial
该文档讲述了如何使用 Data Binding
库来编写申明式的布局,在构建应用逻辑和布局时,如何最少化代码。
该库非常灵活,并且兼容性好。它是一个 support
库,因此,在 Android 2.1(API level 7)
及以上的版本上,都可以使用它。
Gradle Android Plugin
的版本要求是 1.5.0-alpha1
及以上。
构建环境
在 module
的 build.gradle
文件中添加如下的配置:
- android {
- ....
- dataBinding {
- enabled = true
- }
- }
如果应用的某个 module
依赖了使用 data binding
的库,那么,该 module
也必须在自己的 build.gradle
文件中进行上述的配置。
当然,Android Studio
的版本也得 1.3
及以上才行。
数据与布局文件的绑定
data binding 表达式
data binding
布局文件与正常的布局文件大不相同,它的根节点是 layout
元素,layout
中的两个直接子元素是:data
、view
。view
元素即是非 data binding
的布局文件中的任意根节点:
- <?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"/>
布局中,使用 @{}
表达式来为属性赋值。下面的代码中,把 User
对象的 firstName
属性的值赋给了 TextView
:
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName}"/>
数据对象
现在,假定你已经有了 User
对象的 POJO
,如下:
- public class User {
- public final String firstName;
- public final String lastName;
- public User(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- }
- }
这样对象的数据永远不会改变。在应用的,这种情况通常是数量一旦被读取,就不能再改变了。因此,可以使用 JavaBean
对象:
- 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
来说,这两个类是等价的。TextView
控件的值使用了 @{user.firstName}
表达式作为 android:text
属性的值,对于前一个类来说,该表达式将获取 firstName
变量的值,对于后一个类来说,该表达式将获取 getFirstName()
方法的值。如果 firstName()
方法存在,作为一种选择,表达式也可以从这个方法中获取值。
绑定数据
默认地,会基于布局文件的名称自动生成绑定类,转换规则是 Pascal
并在名称添加后缀:Binding
。上面示例中,布局文件的名称是:main_activity.xlm
,因此,自动生成的绑定类就是:MainActivityBinding
。绑定类将会持有所有从布局文件(例如:user
这个变量)到视图绑定的引用,这样,系统才能知道如何给绑定表达式赋值。这就意味着,在 inflating
时创建绑定是一件非常容易的事情:
- @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);
- }
上面的代码就完成了所有的工作。运行应用,你将在界面上看到 User
对象的相关信息。下面的代码也可以完成 inflating
操作:
- MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果,你正在 ListView
或 RecycleView
的适配器中为 item
使用数据绑定,那你应该这样写:
- ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
-
- ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件处理
data binding
可以写事件处理的表达式,并把它分发给 view
。事件属性名称由监听方法的名称决定,但是有一些变化。例如:View.onLongClickListener
所对应的事件方法就是 onLongClick()
,因此,该事件对应的属性就是:android: onLongClick
。要处理一个事件,有两种方法:
方法引用:在表达式中,可以引用与监听方法签名一致的方法。当一个表达式被当作一个方法引用时,data binding
会在一个监听器中把方法引用和宿主对象包裹起来,然后把该监听设置给目标 View
。如果表达式最终的值为 null
,那么,data binding
就不会创建监听,而是设置一个 null
监听。
监听绑定:data binding
总是创建监听,并把它设置给 view
。当事件被分发,监听转换成 lambda
表达式。
方法引用
事件可以直接绑定到处理方法上,与 android:onClick
的属性值可以指定为 Activity
中的某个方法类似。与 View#onClick
属性相比,这种方法的主要优点就是:在编译的时候,表达式就被处理了。因此,如果方法不存在或者方法签名不正确,在编译时期就能知道了。
方法引用和监听绑定最大的不同在于:当数据绑定时,就会创建真正的监听实现,而不是事件触发时才创建。如果你更喜欢在事件发生时使用表达式,那你应该使用监听绑定。
要给一个事件指定处理方法,需要使用绑定表达式,调用方法的名称就可以了。例如:
- public class MyHandlers {
- public void onClickFriend(View view) { ... }
- }
绑定表达式可以把点击的监听指定给某个 View
:
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="handlers" type="com.example.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>
注意:表达式中方法的签名必须与原方法签名完全一致。
监听绑定
监听绑定就是绑定当事件发生时运行的表达式。它与方法引用类似,但是,它可以运行任意的 data binding
表达式。该特性在 Android Gradle Plugin for Gradle 2.0
及以上版本中可用。
在方法引用中,方法的参数必须完全匹配事件监听方法的参数。但是在监听绑定中,只要方法的返回值与监听的返回值一致就可以了(除非是 void
)。例如:
- public class Presenter {
- public void onSaveClick(Task task){}
- }
绑定:
- <?xml version="1.0" encoding="utf-8"?>
- <layout xmlns:android="http://schemas.android.com/apk/res/android">
- <data>
- <variable name="task" type="com.android.example.Task" />
- <variable name="presenter" type="com.android.example.Presenter" />
- </data>
- <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
- <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
- android:onClick="@{() -> presenter.onSaveClick(task)}" />
- </LinearLayout>
- </layout>
监听采用了 lambda
的写法,只有当它是表达式的根元素时才可以。如果表达式中存在回调,data binding
会自动创建这些监听,并为事件注册它们。当 view
触发了事件,data binding
就会对表达式进行转换。As in regular binding expressions, you still get the null and thread safety of Data Binding while these listener expressions are being evaluated.
你应该注意到了,我们并没有定义用于传递给 onClick(android.view.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
对象而不能转换,data binding
将会返回该对象默认的值。例如,对象是 null
,int
是 0
,boolean
是 false
等。
如果在表达式中需要用到判断(例如三元运算符),可以把 void
当作符号使用:
- android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的监听
监听表达式的功能强大,代码也易于阅读。从另一方面来说,包含复杂表达式的监听会使布局难于阅读和维护。所以,表达式应该尽可能简单,只是把可用的数据从 UI
传递给回调方法就可以了,把需要实现的业务逻辑放入方法中实现。
存在一些特殊的点击事件,这些事件不能使用 android:onClick
属性,因为这样会引起混乱。下面属性的创建就是用于避免混乱:
Class |
Listener Setter |
Attribute |
SearchView |
setOnSearchClickListener(View.OnClickListener) |
android: onSearchClick |
ZoomControls |
setOnZoomInClickListener(View.OnClickListener) |
android: onZoomIn |
ZoomControls |
setOnZoomOutClickListener(View.OnClickListener) |
android: onZoomOut |
布局细节
import
data
节点中,可以使用一个或多个 import
,import
语句使得在布局文件中使用 Java
类成为可能,与 Java
类中的使用一样:
- <data>
- <import type="android.view.View"/>
- </data>
经过上述的定义,View
类就可以在表达式中使用了:
- <TextView
- android:text="@{user.lastName}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
如果同一个布局文件中有相同的类名发生混淆,可以使用 alias
属性为类设置别名:
- <import type="android.view.View"/>
- <import type="com.example.real.estate.View"
- alias="Vista"/>
经过上述设定,Vista
就代表着 com.example.real.estate.View
,View
代表着 android.view.View
。引入的类型可以在 variable
或表达式中使用:
- <data>
- <import type="com.example.User"/>
- <import type="java.util.List"/>
- <variable name="user" type="User"/>
- <variable name="userList" type="List<User>"/>
- </data>
- <TextView
- android:text="@{((User)(user.connection)).lastName}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
表达式中,还可以使用引入的类中的静态变量和方法:
- <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.lang.* 会被自动导入·
variable
在 data
中,可以使用任意数量的 variable
。variable
用于描述属性,在 layout
中的属性可以在布局文件的表达式中使用。
- <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 colletion
,将会使用类型进行反射。如果变量是一个没有实现 Observable*
的基本类或接口,那么,变量将不会被观察。
如果对于不同的配置(例如:横竖屏)采用了不同的布局文件,那么,变量将会合二为一。在这样的布局文件中,变量的定义一定不能冲突。
对于每一个申明的变量来说,在生成的绑定类中,都会为它们生成 getter
和 setter
方法。在 setter
被调用之前,变量的值是对应 Java
类型的默认值 – 对象是 null
,int
是 0
,boolean
是 false
等。
在表达式中,如果有需要,会生成名为 context
的特殊变量,它的值来源于根 View
的 getContext()
方法。如果在布局文件中申明了同样名称的变量,该变量将会被重写。
自定义绑定类的名称
默认地,绑定类是根据布局文件的名称生成,以大写字母开始,去掉下划线(_
)并把每个单词的首字母大写,最后添加后缀 Binding
。这些自动生成的绑定类将被放置于模块下的 databinding
包内。例如,名为 contact_item.xml
的布局文件将生成名为 ContactItemBinding
的文件。如果模块的包名为 com.example.my.app
,那么,绑定文件将放置于 com.example.my.app.databinding
包下。
使用 data
元素的 class
属性,可以修改绑定类生成的名称和放置的地方,例如:
- <data class="ContactItem">
- ...
- </data>
上面示例的布局文件生成的绑定类名为:ContactItem
,放置的地方是模块下的 databinding
包。如果要放在不同的包,请加前缀 .
。
- <data class=".ContactItem">
- ...
- </data>
在这种情况下,ContactItem
类将直接放置于模块的包下。如果提供了完整的包路径,那么可以使用任意的路径:
- <data class="com.example.ContactItem">
- ...
- </data>
include
通过应用的全名空间和属性名,变量可以从容器布局中传递到被包含的布局中:
- <?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
文件中,必须得有名为 user
的变量。
data binding
不支持根元素为 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>
- <include layout="@layout/name"
- bind:user="@{user}"/>
- <include layout="@layout/contact"
- bind:user="@{user}"/>
- </merge>
- </layout>
表达式
一般特性
表达式看起来与 Java
表达式很类似,下面是相同部分:
示例:
- android:text="@{String.valueOf(index + 1)}"
- android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
- android:transitionName=‘@{"image_" + id}‘
缺失的操作符
相比能在 Java
中使用的语法来说,表达式缺失了少量的操作符:
null 的合并操作符
该操作符的意思是:如果左边的表达式不为 null
,最后的表达式的值为左边表达式的值。如果为 null
,是右边表达式的值。
- android:text="@{user.displayName ?? user.lastName}"
上面示例代码的功能相当于下面代码的功能:
- android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
该形式在一开始就讨论过了:JavaBean
引用的短格式:
- android:text="@{user.lastName}"
避免 NullPointerException
生成的数据绑定代码会自动检查 null
来避免空指针异常。例如,@{user.name}
,如果 user
为 null
,user.name
将被指定为默认值(null
)。如果引用了 user.age
,age
的类型是 int
,那么默认值就是 0
。
集合
常用的集合:array
,list
,sparse 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]}"
字符字面值
如果对属性值使用了单引号,那么表达式就可以使用双引号了:
- android:text=‘@{map["firstName"]}‘
当然,可以对属性值使用双绰号,这时,字符字面值就应该使用 ` 符号:
- android:text="@{map[`firstName`}"
- android:text="@{map[‘firstName‘]}"
资源
使用正常的语法就可以在表达式中获取资源:
- android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
对 string
和 plurals
来说,也可以提供参数对其进行格式化:
- android:text="@{@string/nameFormat(firstName, lastName)}"
- android:text="@{@plurals/banana(bananaCount)}"
如果 plural
有多个参数,那么所有的参数都应该被传递:
- Have an orange
- Have %d oranges
-
- 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 |
数据对象
所有 POJO
都可以用于数据绑定,但是,修改 POJO
不会引起 UI
的更新。数据绑定的真正强大之处在于:数据变化了,有能力通知数据对象。数据变化通知机制有三种:Observable object
,Observable field
,Observable collection
。
当这三种可观察数据对象中的某一种类型的数据绑定到 UI
时,数据对象的属性发生了变化,UI
将会被自动更新。
Observable 对象
实现了 Observable
接口的类允许绑定附着一个监听,把对象和监听绑定到一起,用于观察对象所有属性的变化。
Observable
接口有添加和移除监听的机制,但是,通知却是取决于开发者。为了使开发简单,创建了一个实现了监听注册机制的基本类:BaseObservable
。当属性变化时,数据实现类仍然会响应通知。要实现这样的效果,需要在 getter
方法上添加 @Bindable
注解,并在 setter
方法中进行通知:
- 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
类(该类的模块的包下)中生成一个实体。如果数据类的基类不能被改变,那么,也可以使用便捷的 PropertyChangeRegistery
来实现 Observable
接口,有效的存储和通知监听。
ObservableField
在创建 Observable
类时,会引入一些工作量,因此,想节约时间的开发者、或拥有少量属性的类可以使用 ObservableField
类型。它有些同辈:ObservableBoolean
,ObservableByte
,ObservableChar
,ObservableShort
,ObservableInt
,ObservableLong
,ObservableFloat
,ObservableDouble
,ObservableParcelable
。
- 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();
Observable 集合
应用通常使用一些动态的结构来存储数据。Observable
集合可以通过 key
值获取数据对象。当 key
是一个引入类型时,ObservableArrayMap
类型非常有用,例如 String
:
- ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
- user.put("firstName", "Google");
- user.put("lastName", "Inc.");
- user.put("age", 17);
布局文件中,可以通过 key
获取到值:
- <data>
- <import type="android.databinding.ObservableMap"/>
- <variable name="user" type="ObservableMap<String, Object>"/>
- </data>
- …
- <TextView
- android:text=‘@{user["lastName"]}‘
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
- <TextView
- android:text=‘@{String.valueOf(1 + (Integer)user["age"])}‘
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
当 key
是整形时,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"/>
生成绑定
自动生成的绑定类链接了是布局文件中 View
与布局 variable
之间的桥梁。正如前期讨论的那样,绑定类的名称和存放位置可以被自定义。所有生成的绑定类都继承于 ViewDataBinding
。
创建
在 inflation
之后,绑定会立即创建,这样可以确保:视图层次不会先于视图与表达式绑定动作之前发生。找到绑定布局有几种方式,最常用的就是使用绑定类的静态方法。inflate
方法会一次性渲染视图层次并进行绑定:
- MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
- MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局已经使用其它的机制进行了渲染,那就单独进行绑定:
- MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时候,绑定不能被预告识别,这时就得使用 DataBindingUtil
类:
- ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
- parent, attachToParent);
- ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
带 ID 的 View
将会为布局文件中每个带有 ID
的 View
生成类型为 public final
的字段。绑定其实做了一个 View
层次方面的单向传递,它会抽取 View
的 ID
。该机制的速度比调用 findViewById
方法的速度更快,例如:
- <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>
上述代码的自动生成类中,将包含如下两个字段:
- public final TextView firstName;
- public final TextView lastName;
不使用数据绑定,ID
不是必须的,但是数据绑定中使用 ID
,可以在代码中获取 View
。
变量
绑定将会自动为每个变量生成 accessor
方法:
- <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>
上述的代码将在绑定类中生成如下的代码:
- 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
略微不同。它会一直不可见,除非把它设置为可见,或者显式地渲染它。
因为从本质上说,ViewStub
不会出现在 View
层次上,the View in the binding object must also disappear to allow collection. Because the Views are final, a ViewStubProxy object takes the place of the ViewStub, giving the developer access to the ViewStub when it exists and also access to the inflated View hierarchy when the ViewStub has been inflated.
When inflating another layout, a binding must be established for the new layout. Therefore, the ViewStubProxy must listen to the ViewStub’s ViewStub.OnInflateListener and establish the binding at that time. Since only one can exist, the ViewStubProxy allows the developer to set an OnInflateListener on it that it will call after establishing the binding.
高级绑定
动态变量
有时,某个绑定总是不能被识别。例如,操作任意布局的 RecycleView.Adapter
将不能识别特定的绑定类。只有当 onBindViewHolder(VH, int)
时,才会指定绑定的值。
在这种情况下,RecycleView.Adapter
绑定的所有布局都有一个 item
变量。BindingHolder
的 getBinding
方法将返回基本的 ViewDataBinding
类:
- public void onBindViewHolder(BindingHolder holder, int position) {
- final T item = mItems.get(position);
- holder.getBinding().setVariable(BR.item, item);
- holder.getBinding().executePendingBindings();
- }
即时绑定
当变量或可观察者改变时,在下一个 frame
之前,绑定将会按计划进行改变。但是有时候,绑定需要立即被执行。这时,就需要使用 executePendingBindings()
方法。
后台线程
只要数据模型不是集合,就可以在后台对数据模型进行改变。在进行这样操作的时候,数据绑定会本地化每个变量和字段,这样可以避免任何迸发性的问题。
属性 setter 方法
每当绑定的值发生了变化,View
的表达式如果使用了变量,自动生成的绑定类必须调用 setter
方法。
自动生成的 settter
对于任一属性来说,数据绑定会尝试寻找设置属性的方法。属性的全名空间没有关系,仅仅与属性名称有关。例如,TextView
控件的 android:text
属性使用了表达式,那将会查找 setText(String)
这个方法。如果表达式返回的是整形,那么数据绑定将会查找 setTExt(int)
这个方法。务必得让表达式返回正确的类型,如果有需要,对返回值进行类型转换。注意:即使属性名称不存在,数据绑定也会进行。通过数据绑定,很容易就可以为任何 setter
“创建”属性。例如,支持库的 DrawerLayout
没有任何的属性,但是有大量的 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 进行重命名
某些属性的 setter
方法与属性名称并不匹配。对于方法来说,可以使用 BindingMethods
注解来给属性指定 setter
方法。该注解得写在类上,类中还得包含使用 BindingMethod
注解的方法。例如,下面代码中的 android:tint
属性与 setImageTintList(ColorStateList)
相关联,而不是与 setTint
方法关联。
- @BindingMethods({
- @BindingMethod(type = "android.widget.ImageView",
- attribute = "android:tint",
- method = "setImageTintList"),
- })
开发者对 setter
进行重命名的情况不多,因为 Android
框架已经实现了系统属性的 setter
。
自定义 setter
某些属性需要自定义绑定逻辑。例如,android:paddingLeft
属性没有相关的 setter
方法。但是,setPadding(left, top, right, bottom)
方法存在。开发者可以使用拥有 BindingAdapter
注解的静态方法来自定义某个属性调用的 setter
方法。
- @BindingAdapter("android:paddingLeft")
- public static void setPaddingLeft(View view, int padding) {
- view.setPadding(padding,
- view.getPaddingTop(),
- view.getPaddingRight(),
- view.getPaddingBottom());
- }
绑定适配器对于自定义的类型非常有用。例如,自定义的 loader
可以在非主线程上加载图片。
如果发生了冲突,开发者自定义的绑定适配器会覆盖默认的适配器。
接收多个参数的适配器写法如下:
- @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);
- }
- <ImageView app:imageUrl="@{venue.imageUrl}"
- app:error="@{@drawable/venueError}"/>
上面的适配器在 imageUrl
和 error
上都会被调用,并且一个类型是 String
,一个类型是 Drawable
。
自定义命名空间将被忽略
可以在 Android
命名空间下写适配器
绑定适配器在处理中,也可以使用旧值。一个方法如果既有新值又有旧值,应该先旧值,后新值:
- @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.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow()
和 onViewDetachedFromWindow()
。因此,必须定义两个接口来区分和处理:
- @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
- public interface OnViewDetachedFromWindow {
- void onViewDetachedFromWindow(View v);
- }
-
- @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
- public interface OnViewAttachedToWindow {
- void onViewAttachedToWindow(View v);
- }
因为一个接口的改变会影响另外一个接口,因此,必须使用三个不同的绑定适配器,每个属性一个,然后两个合起来一个:
- @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
单独使用添加或移除的监听,而不是集合方法:View.OnAttachStateChangeListener
。android.databinding.adapters.ListenerUtil
会保持前置监听的跟踪,因此,就可以从适配器中适配它们了。
上面的方法添加了 @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解,这是用于告诉数据绑定代码生成器:只有程序运行在 Honeycomb MR1
及以上版本的设备上时,才会生成这些监听。因为只有这些设备,才支持 addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
方法。
转换器
对象转换
如果绑定表达式返回的是对象,那么,将会从 自动生成的、重命名的、自定义的 setter
中查找合适的 setter
。最终,对象将被解析为所选择 setter
的参数类型。
对于使用 ObservableMap
类型存储数据的变量来说,下面的代码是便捷的获取方法:
- <TextView
- android:text=‘@{userMap["lastName"]}‘
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
上面的代码中,userMap
返回一个对象,生成器会找到 setText(String)
这个 setter
方法,并把这个对象进行解析。如果有参数类型可能发生混淆的情况存在,就需要开发者自己处理。
自定义转换
有时候,在指定的类型之间,转换应该自动进行。例如:
- <View
- android:background="@{isError ? @color/red : @color/white}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
上面的代码中,background
属性需要 Drawable
类型,但是 color
是 int
类型。当期望值类型是 Drawable
而返回值类型是 int
时,int
值就会被转换为 ColorDrawable
类型。这个转换是在有 @BindingConversion
注解的静态方法中进行的:
- @BindingConversion
- public static ColorDrawable convertColorToDrawable(int color) {
- return new ColorDrawable(color);
- }
注意:转换只能发生在 setter 级别,并且,不允许混合类型。例如:
- <View
- android:background="@{isError ? @drawable/error : @color/white}"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
Android Studio 对数据绑定的支持
对于数据绑定代码来说,Android Studio
支持许多代码级的编辑特性。例如,支持数据绑定的如下特性:
语法高亮
表达式语法错误标识
xml
代码完成
引用和快速文档
注意:Array
和 generic type
(例如 Observable
类)有时可能会显示错误,尽管这里真的没有错误。
如果提供了表达式的默认值,预览面板将会显示该值:
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@{user.firstName, default=PLACEHOLDER}"/>
在项目的设计阶段,如果需要显示默认值,可以使用工具,而不是使用默认值,在 这里 有介绍。