本文翻译自文章:Data Binding Library
翻译人员:麦田里的守望者-Lost
例子下载地址:DataBinding-Example
这篇文章介绍了如何使用Data Binding Library来编写声明式布局,和将中间代码必要性降低到最少来绑定应用程序的逻辑和布局。
Data Binding Library即提供了灵活性又提供了广泛的兼容性-它是一个支出库,可以在Android 2.1(API 7)版本以上使用。
要使用数据绑定,需要Gradle 1.5.0-alpha1或更高版本。
搭建环境
在开始使用Data Binding前,需要在Android SDK manager中下载库。
要配置应用程序使用数据绑定,需要在app模块(module)的build.gradle文件中添加dataBinding元素。
使用下面的代码来配置数据绑定:
android {
....
dataBinding {
enabled = true
}
}
如果在app模块中有需要依赖使用数据绑定的库,这个模块必要在的build.gradle文件中配置数据绑定。
Android Studio 1.3或者更高的版本才支持数据绑定。
数据绑定布局文件
编写第一组数据绑定表达式
Data-binding文件有点稍微不同,它的根标签是layout,接着是一个data元素和view的根元素。它看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.wj.study.domain.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:text="@{user.firstName}"
android:textSize="@dimen/textSize_16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:text="@{user.lastName}"
android:textSize="@dimen/textSize_16sp" />
</LinearLayout>
</layout>
data中的user变量描述了一个可以用在layout中的属性。
<variable
name="user"
type="com.wj.study.domain.User" />
布局中的表达式用”@{}”语法来编写属性。在这里,TextView的text设置为user的firstName属性:
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:text="@{user.firstName}"
android:textSize="@dimen/textSize_16sp" />
数据对象
假设现在有一个普通的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;
}
}
这种类型的对象有永远不会改变的数据。它也可以使用一个JavaBeans对象:
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;
}
}
从数据绑定的角度来看,这两个类是相等的。被用于TextView的android:text属性中的表达式@{user.firstName},在前一个类中访问的是firstName域,在后一个类中访问的是getFirstName()方法。另外,如果方法存在,它还将决定firstName()方法。
绑定数据
默认情况下,会基于布局文件的名字生成一个Binding类,并将它转换为Pascal用例和以”Binding”结尾。上面的布局文件为activity_user.xml,因此生成的类为ActivityUserBinding。这个类持有从布局属性(如user变量)到布局view所有的绑定,而且知道如何分配值给绑定表达式。对于创建绑定最简单的方式是在填充的时候:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
User user = new User("Wang", "Jiang");
binding.setUser(user);
}
另外,可以也可以通过下面的方式获得view:
ActivityUserBinding view = ActivityUserBinding.inflate(getLayoutInflater());
如果在ListView或RecyclerView适配器中使用数据绑定条目,可以这样用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
事件处理
Data Binding允许编写表达式来处理分发给view的事件(如onClick)。事件属性名字由监听器方法名字管理。例如,View.OnLongClickListener有一个onLongClick()方法,因此事件的属性是android:onLongClick。这里有两种方式来处理事件:
方法引用:在表达式中,可以引用与监听器方法签名一致的方法。当一个表达式评估方法引用的时候,Data Binding包装了方法引用和监听器宿主对象,并将监听器设置到了目标view上。如果表达式评估为null,Data Binding不会创建一个监听器,而且会将监听器设置为null。
监听器绑定:它们是在事件发生时评估的lambda表达式。Data Binding总会创建一个监听器来设置给view。当事件分发时,监听器会评估lambda表达式。
方法引用
事件可以直接绑定到处理方法上,与android:onClick在Activity中分配方法的方式相似。相比View#onClick属性,一个重要的优势是表达式在编译时处理,因此,如果方法不存在或它的签名不正确,将会发生编译时错误。
方法引用和监听器绑定之间主要的不同是,当数据绑定时真正的监听器实现才创建,而不是事件触发时。如果更喜欢在事件发生时评估表达式,可以使用监听器绑定。
为了将事件分给它的处理者,使用一个普通的绑定表达式,并以值作为调用的方法名称。例如,如果数据对象有两个方法:
public void onClickFriend(View view) {
Log.d("TAG","onClickFriend");
}
这个绑定表达式可以将点击监听器分给view:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="handlers"
type="com.wj.study.handler.MyHandlers" />
<variable
name="user"
type="com.wj.study.domain.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:onClick="@{handlers::onClickFriend}"
android:text="@{user.firstName}"
android:textSize="@dimen/textSize_16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:text="@{user.lastName}"
android:textSize="@dimen/textSize_16sp" />
</LinearLayout>
</layout>
注意:表达式中的方法签名必须与监听器对象中的方法签名匹配。另外,还需在Activity中设置事件处理。
ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
User user = new User("Wang", "Jiang");
binding.setUser(user);
binding.setHandlers(new MyHandlers());
监听器绑定
当事件发生时,监听器绑定将绑定正在运行的表达式。虽然和方法引用相似,但是它可以运行任意的数据表达式。这个功能适合Gradle 2.0或更高版本以上。
在方法引用中,方法参数必须与事件监听器参数匹配。在监听器绑定中,返回值必须与监听器期望返回值相匹配。例如,有一个Presenter类:
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="handlers"
type="com.wj.study.handler.MyHandlers" />
<variable
name="handlers"
type="com.wj.study.handler.Presenter" />
<variable
name="user"
type="com.wj.study.domain.User" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:onClick="@{handlers::onClickFriend}"
android:text="@{user.firstName}"
android:textSize="@dimen/textSize_16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:onClick="@{() -> presenter.onSaveClick(task)}"
android:text="@{user.lastName}"
android:textSize="@dimen/textSize_16sp" />
</LinearLayout>
</layout>
监听器由仅仅作为表达式根元素允许的lambda表达式所表示。当在表达式中使用回调方法,Data Binding会自动创建事件和注册监听器。当view触发事件,Data Binding会执行给予的表达式。正如绑定表达式的规则,当监听器表达式正在被评估的时候,仍然可以获得Data Binding的线程安全性。
注意:在上面的例子中,并没有定义传递给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将返回这个类型的默认Java值。例如,引用类型为null,0为int,false为boolean等等。
如果需要使用断言表达式(如三元运算),可以用void作为符号。
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂监听器
监听器表达式非常强大,而且让代码非常简单易读。另一方面,监听器含有复杂的表达式会让布局难以阅读和维护。这些表达式应该像从UI传递可用的数据到回调方法那么简单。应该从监听器表达式执行的回调方法里实现任何业务逻辑。
这里有一些专门的点击事件助手存在,它们需要一个不同于android:onClick的属性来避免冲突。以下属性就是创建来避免冲突的:
布局细节
导入
在数据元素中可以没有或import元素。在布局文件中它们允许你引用这些类,就像在Java中一样。
<data>
<import type="android.view.View"/>
</data>
现在,在绑定表达式里可以这样写:
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/size_45dp"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:onClick="@{handlers::onClickFriend}"
android:text="@{user.firstName}"
android:textSize="@dimen/textSize_16sp"
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。导入的类型可以作为变量和表达式的引用类型:
<data>
<import type="android.view.View" />
<import type="com.wj.study.domain.User" />
<import type="java.util.List" />
<variable
name="handlers"
type="com.wj.study.handler.MyHandlers" />
<variable
name="user"
type="User" />
<variable
name="userList"
type="List<User>" />
</data>
注意:Android Studio还没有处理导包,因此自动导入变量可能不会在IDE中起作用。应用程序仍然可以通过编译,而且通过全名限定来定义变量来解决IDE问题。
<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中一样,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>
变量类型在编译时检查,因此,如果一个variable实现了Observable或者是一个observable collection,应该在类型中反应出来。如果variable是一个没有实现Observable接口的基类或接口,这个variable不会被观察到。
当对不同的配置(如landscape 或portrait)有不同的布局时,variable将被合并。在这些布局文件之间绝不能有相互冲突的变量定义。
生成的binging类对每一个描述的variable都有一个setter和getter。variable会选用默认的Java值直到setter被调用-引用类型为null,int类型为0,boolean类型为false等等。
根据需要,在使用绑定表达式中生成了一个特殊的context变量。context的值来自根view的getContext()。context变量将被以这个名字声明的显示变量所重写。
自定义绑定类名字
默认情况下,基于布局文件的名字生成一个Binding类,以大写开始,去掉下划线(_),用接下来的字母变成大写,然后以”Binding”结尾。这个类将被放在模块包下的databinding包中。例如,布局文件是contact_item.xml,生成的类将是ContactItemBinding。如果模块包是com.example.my.app,它将被放在com.example.my.app.databinding。
通过调整data元素的类属性来将Binding类重新命名或放置在不同的包中。例如:
<data class="ContactItem">
...
</data>
这生成的绑定类以ContactItem放在模块包下的databinding包中。如果类应该被生成在模块包下的不同包中,它的前缀必须以”.”:
<data class=".ContactItem">
...
</data>
在这个例子中,ContactItem直接生成在模块包中。如果提供了包全名,任何包都可以使用:
<data class="com.example.ContactItem">
...
</data>
includes
变量通过使用应用程序命名空间和属性变量名,从包含布局中传递给include布局的绑定(bind:user=”@{user}”):
<?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>
这里,必须有一个user变量在name.xml和contact.xml文件中。
数据绑定不支持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>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
表达式语言
共同特征
表达式语言与Java表达式非常相似,下面为相同的:
- 数学运算 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二元 & | ^
- 一元 + - ! ~
- 位移 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 分类 ()
- 字面量 - character, String, numeric, null
- 转换
- 方法调用
- 域访问
- 数据访问 []
- 三元操作符 ?:
例子:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
缺失操作
缺失一些在Java中可以使用的表达式语法:
- this
- super
- new
- Explicit generic invocation
Null合并运算符
null合并运算符(??),如果不为null选择左边的操作数,如果为null选择右边的操作数。
android:text="@{user.displayName ?? user.lastName}"
这在功能上相当于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
这在上面的“编写第一组数据绑定表达式”中已经讨论了。当一个表达式引用类中的属性,对fields, getters, 和ObservableFields都是使用相同的格式。
android:text="@{user.lastName}"
避免空指针异常
生成的数据绑定代码会自动检查空指针和避免空指针异常。例如,在表达式@{user.name}中,如果user为空,user.name将被分配默认值(null),如果引用user.age,age为int类型,它的默认值会是0。
集合
常见的集合:arrays, lists, sparse lists, 和 maps,为方便可以使用[ ]操作符来访问。
<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}"
格式字符串和复数可根据提供的参数来评估:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当一个复数需要多个字符串时,所有参数都应该通过:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
有些资源需要显式类型的评估。
数据对象
任何普通的Java对象(POJO)都可以使用数据绑定,但是修改一个POJO不会使UI更新。数据绑定真正的力量在于,当数据改变时,给定的数据对象有能力去通知。这里有三种不同的数据改变通知机制,Observable 对象,Observable 字段,Observable 集合。
当这些observable数据对象绑定到UI时,数据对象的一个属性改变,UI就会自动更新。
Observable 对象
一个实现了Observable接口的类,允许绑定系一个单独的监听器到绑定对象上来监听对象上所有属性的改变。
Observable接口有一个机制来添加和删除监听器,但是通知由开发者开决定。为了让开发更简单,一个基类,BaseObservable(为了实现监听器注册机制被创建的)。当属性改变时,数据类实现这依然负责通知。这是通过分配一个Bindable 注解给getter和在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类文件入口。BR类文件会在模块包(module package)中生成。如果数据类的积累不能被改变,Observable接口可以使用PropertyChangeRegistry存储和通知监听器来实现。
Observable 域
一点点工作参与到创建Observable 类中,想节约时间或只有几个属性的开发者,可以使用ObservableField 和它的兄弟姐妹 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 ObservableParcelable。ObservableField 是独立的有一个单独域的observable 对象。原始版本在访问操作期间避免了装箱和拆箱。要使用,在数据类中创建一个公共的最终的(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();
}
要访问值,使用set和get存取方法:
user.firstName.set("Google");
int age = user.age.get();
Observable 集合
一些应用程序使用更多的动态结构来保存数据。Observable集合允许键来访问那些数据对象。当键是引用类型(如String)时,使用ObservableArrayMap。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局中,map可以通过String 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"/>
当键是整型时,使用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关联在了一起。正如前面讨论的,Binding的名字和包可以自定义。生成的绑定类都继承自ViewDataBinding。
创建
在填充之后,绑定将会尽快被创建,以确保View层次结构在布局中与表达式的View绑定之前不被打扰。这里有绑定到布局的几种方式。最常用的是在Binding类上使用静态方法。填充方法填充View层级结构,而且一步绑定。这里有更简单的版本,只需要一个LayoutInflater 和一个ViewGroup :
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层次结构上的绑定做了一个单一的传递,用于提取有ID的View。对于一些View,这种机制比调用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是完全没有必要的,但是这里仍然有一些用于访问View的实例是依然在代码中需要的。
变量
每一个变量将给予一个访问方法:
<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>
将在绑定中生成setter和getter:
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)
ViewStubs
与普通View中的ViewStub有一点不同。它们一开始是不可见的,而且当它们是可见的或明确告知填充的时候,它们通过填充另一个布局来取代布局中的自己。
因为ViewStub本质上在View层级结构中是不可见的,在绑定对象中的View也必须不可见以便收集。因为View是最终的(final),一个ViewStubProxy 对象会取代ViewStub,当它存在的时候,让开发者访问ViewStub,而且ViewStub被填充的时候也可以访问填充的View层级结构。
当填充另一个布局的时候,必须为新的布局建立一个绑定。因此,ViewStubProxy必须监听ViewStub的ViewStub.OnInflateListener而且在绑定的那个时候建立。因为只有一个可以存在,ViewStubProxy允许开发者在它上面设置一个OnInflateListener,而且在建立绑定之后会回调它。
提前绑定
动态变量
有时,具体的绑定类将不会被知道。例如,RecyclerView.Adapter可以操作任意的布局却不知道具体的绑定类。在调用onBindViewHolder(VH, int)方法的时候,它仍然分配绑定值。
在这个例子中,RecyclerView绑定的所有布局都有一个”item”变量。BindingHolder 有一个返回ViewDataBinding 基类的getBinding 方法。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立刻绑定
当一个variable或observable改变时,绑定将在下一帧之前列入到改变计划表中。有时候,然后,当绑定必须立被执行。为了强迫执行,使用executePendingBindings()方法。
后台线程
只要不是一个集合,就可以在一个后台线程中改变数据模型(data model)。数据绑定会将每一个变量/域本地化,同时进行评估,以避免任何并发问题。
属性 Setter
无论什么时候一个绑定值发生改变,生成的绑定类必须调用有绑定表达式View的setter方法。数据绑定框架有方式自定义方法来调用设置值。
自动 setter
对于一个属性,数据绑定试着找到setAttribute方法。属性的命名空间并不重要,只有属性名字本身重要。例如,关联TextView的属性android:text的表达式将会查找setText(String)。如果表达式返回一个int值,数据表达式将查找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关联起来。这必须关联一个类,而且包含注解BindingMethods,用于每一个重命名的方法。例如,android:tint属性真正关联的是setImageTintList(ColorStateList),而不是setTint。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
开发者不需要重命名setter,android框架属性已经实现了。
自定义setter
一些属性需要自定义绑定逻辑。例如,对于android:paddingLeft属性,这里没有关联的setter。而是,setPadding(left, top, right, bottom)存在。静态绑定适配器方法与BindingAdapter 注解允许开发者自定义属性的setter如何被调用。
android属性已经有BindingAdapters 创建。例如,下面是一个paddingLeft:
@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都被ImageView使用,而且imageUrl是string,error是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使用add和remove监听器,而不是对View.OnAttachStateChangeListener使用设置方法。为了在Binding Adaper中让它们可以被删除,android.databinding.adapters.ListenerUtil帮助追踪先前的监听器。
通过OnViewDetachedFromWindow和OnViewAttachedToWindow 的注解@TargetApi(VERSION_CODES.HONEYCOMB_MR1),当运行在Honeycomb MR1和新设备上时,数据绑定代码生成器知道监听器应该被生成,相同的版本由addOnAttachStateChangeListener(View.OnAttachStateChangeListener)支持。
转换器
对象转换
当一个对象从表达式中返回时,一个setter将被从自动, 重命名, 和自定义setters中选出来。在选中的setter中,这个对象将被转换为参数类型。
这是为那些使用ObservableMaps 保存数据方便。例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这个userMap返回一个对象,这个对象将自动转换为在setter setText(CharSequence)中找到的参数类型。关于参数类型,这里可能有点混淆,开发者需要在表达式中进行转换。
自定义转换
有时转换应该是特定类型之间的自动转换。例如,在设置背景时:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在这里,背景需要的是Drawable,而不是整型的颜色值。每当期望的是一个Drawable,返回的却是一个整型值,整型应该被转换为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代码补全
注意:数据和泛型类型,如Observable类,没有错误也可能呈现错误。
如果有提供的话,预览面板会为数据绑定表达式呈现默认值。在下面的例子中,从布局XML文件中摘录了一个元素,在TextView中,预览面板呈现了一个 PLACEHOLDER 默认文本值。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=PLACEHOLDER}"/>
如果在项目设计阶段期间需要展示默认值,可以使用工具(tools)属性,而不是默认表达式值,正如Designtime Layout Attributes中描述的一样。