# 背景
略。
# 环境设置
要开始使用databinding,需要在module里面的build.gradle配置好相关的环境:
具体参照以下代码:
android {
....
dataBinding {
enabled = true
}
}
如果一个library module设置了databinding,那么app module也*必须* 在build.gradle加上以上代码。
# 在布局文件中使用databinding
## 示例代码
数据绑定布局文件和一般布局文件略有不同,它以`<layout>`的根标签开始,后跟`<data>`标签和*视图根元素*,即一般的视图布局。示例文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
其中`<data>`标签里面的`<variable>`标签声明好需要绑定的数据,后面还可以看到`<import>`等标签的使用。
## 绑定语法
* 数据绑定
绑定数据的表达式通过 `@{}` 语法写入标签的属性里面,例如:`android:text="@{user.firstName}"`这句代码就是把user.firstName设置到`android:text`属性里面。
`@{}`里面的表达式,如果是对象引用,例如`user.firstName`,表达式求值的时候,会首先查找对象的getter方法,即`getFirstName()`方法。如果存在此方法,则以返回值作为表达式的值。如果不存在对应的getter,则会查找公开的字段,如果没有相同命名的公开字段,编译时就会报错。
双向绑定的语法是 `@={}` ,需要注意的是这是一项实验性功能,尚未到正式版本。
* 事件处理
事件绑定语法和数据绑定差不多,都是通过 `@{}` 把方法的引用([Java8的语法](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html))给到特定的属性。
但相对于属性,设置方法的语法还是需要留意一下:
* 基本语法:把一个lambda表达式或者通过方法引用语法`::`设置方法到事件属性里面,例如最常见的点击事件
android:onClick="@{(v) -> presenter.onClick()}"
或者是:
android:onClick="@{presenter::onClickView}"
使用`::`语法的时候,需要保证的是`pesenter.onClickView`方法拥有和对应的事件方法同样的参数,即`onClickView`方法的定义必须是下面这样:
public T onClickView(View view){
...
}
* 假如需要方法里面的参数,只要需要其中一个,都要把所有参数写上:
app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}"
* 假如不需要任何方法里面的参数,那么括号里面的参数列表可以省略:
app:onTagClickListener="@{() -> presenter.onLoadMore()}"
* 如果方法需要有返回值,那么你的表达式或者方法的返回类型一定要和属性的返回类型一致,否则会编译不通过。
## 其他布局细节
### Import导包
`<data>`标签里面还支持import语法,具体的格式是下面这样,跟java代码里面差不多:
<data>
<import type="android.view.View"/>
</data>
导包之后就可以使用它的一些静态方法或者静态变量,例如`View.GONE`等等,甚至可以导入`TextUtils`来判断字符串非空。但由于可读性和可维护性方面的问题,我们在项目中应该**禁止**xml中使用任何条件语句。
关于*import*更详细的资料请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#imports)
### Variables变量
`variable`标签会为每个声明的变量赋初始值,如果是引用类型则初始值为null,int类型是0,等等。
xml中有一个隐藏的变量名`context`,它对应的值是布局根元素的`getContext`方法获取的上下文对象。如果你的xml中显式声明`context`变量,那么这个变量会被你的声明所覆盖。
关于*variable*更详细的资料请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#variables)。
### 自定义生成的绑定类名
默认情况下,根据布局文件的名称生成一个Binding类,以大写字母开头,删除下划线`_`并大写后面的字母,然后后缀“Binding”。 该类将放置在模块包下的数据绑定包中。 例如,布局文件contact_item.xml将生成ContactItemBinding。 如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding中。
绑定类可以通过调整`<data>`元素的`class`属性来重命名或放置在不同的包中。 例如:
<data class="ContactItem">
...
</data>
以上设置会把xml生成的对应绑定类重命名为ContactItem,而不是XXXBinding。`class`属性还可以设置相对或者完整包名,具体请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#custom_binding_class_names)
### Inclub标签绑定
略。
[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#includes)。
### 布局中可以使用的表达语言
从可读性和可维护性的角度触发,我们项目中禁止布局文件中做任何逻辑和业务相关的表达式运算,绑定的字段一般就是最终的输出。作为了解,可以查看一下[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#expression_language)。
# 绑定的数据对象
从上面的介绍得知,我们可以把普通的java对象(POJO)或者java bean对象(BEAN)绑定到属性里面,但是当这些对象的属性发生变化的时候,页面是不会随之发送变化的。如果需要属性变化马上反应到视图上,就要使用绑定库提供的`Data Objects`。有以下三种实现方法提供:
* 继承BaseObservable,为需要实时变更的get方法加上`@Bindable`注解,编译器会自动为该方法生成对应的id存放在BR里面(类似R文件)。对应的set方法需要调用`notifyPropertyChanged(id)`,例如:
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);
}
}
* 使用ObservableField和他的同胞 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.使用它们来替代你的类中需要实时变化的字段类型,不需要额外设置get set方法,但修饰符需要是`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();
}
* 使用Observable Collections,例如 ObservableArrayMap 和 ObservableArrayList。在xml中可以使用符号[]访问。但直接使用集合来绑定数据,还是会破坏可读性。在类中使用的话和ObservableField无异。所以有需要了解的请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#observable_collections)。
# 生成的绑定类浅析
前面已经介绍过,编译器会为每一个使用数据绑定的xml生成一个继承`android.databinding.ViewDataBinding`的绑定类。绑定类处理好所有该布局文件的属性绑定和事件处理,并且提供对外的设置绑定对象的方法。这个类可以重命名,可以自定义包名等等,这些在之前已经描述过。
## 创建绑定
在布局被`inflate`之后应该尽快绑定,避免对View的操作造成绑定失败。绑定的方法一般有以下几种:
* 用生成的绑定类`inflate`,会同时执行好布局的`inflate`和绑定,无须额外操作:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
* 如果布局是使用不同的机制来进行`inflate`的,可以使用绑定类的`bind`方法进行绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
* 或者都可以直接用`DataBindingUtil.inflate`方法进行创建布局、生成绑定,相比具体的绑定类,只是多了参数,返回的类型是`<T extends ViewDataBinding>`:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
* 在Activity中可以使用`DataBindingUtil.setContentView`方法类代替activity原有的`setContentView`方法:
ActivityMain2Binding binding = DataBindingUtil.setContentView(activity.this, R.layout.activity_main2);
## 为带id的元素生成引用
简直就是**福音**!!!绑定类会为所有布局中带id的元素生成引用,例如在xml中写了一个TextView是id是`text`,绑定类里面就会生成一个`public final TextView text;`的引用,从此告别`findViewById`,我简直都要哭了~生成的字段名的规则是首字母小写,下划线去除,下划线后的首字母大写。
## 为变量生成代码
绑定类会为定义在`<variable>`标签的对象生成对应的`get` `set`方法,如果有涉及到事件处理,还会根据事件传入方式生成一些监听器或者一些事件回调类:
// variables
@Nullable
private com.healthmall.library.app.databinding.DataBindingListItemInfoVM mItem;
@Nullable
private com.healthmall.library.app.databinding.ListPresenter mPresenter;
@Nullable
private final android.view.View.OnClickListener mCallback4;
// values
// listeners
private OnClickListenerImpl mPresenterOnClickViewAndroidViewViewOnClickListener;
## ViewStub绑定
提供了ViewStubProxy来解决ViewStub的绑定问题,具体请看[ViewStubProxy文档](https://developer.android.com/reference/android/databinding/ViewStubProxy.html)。
## 高级特性
### 动态绑定
编译器会为xml的每个变量生成对应的id,绑定类会为每个变量生成对应的get、set方法,但有时候我们没办法确定具体的id和具体的对象,譬如我们在编写adapter基类,假设我们有一个统一的id `BR.item` ,但所有列表对象都基本不一致。所以需要使用ViewDataBinding(绑定类的基类)提供的`setVariable(bindId, bindObject)`方法来进行动态绑定。
@Override
public final void onBindViewHolder(BaseBindingViewHolder holder, int position) {
// 绑定数据
holder.bind.setVariable(BR.item, list.get(position));
holder.bind.executePendingBindings();
}
### 绑定立即生效
当变量或可观察到的变化时,绑定将被安排在下一帧之前改变。 然而,有时候,绑定必须立即执行。 要强制执行,请使用`executePendingBindings`方法。
### 线程安全
只要不是集合,您可以在后台线程中更改数据模型。 数据绑定将在评估时本地化每个变量/字段以避免任何并发问题。
# 属性设置器
属性设置器是编译器连接代码和布局的一套规则,它决定了绑定类如何生成。除了默认的规则以外,数据绑定框架还提供了自定义的方法,来帮助我们按照自己的规则去建立绑定。作为开发者而言,个人认为这是我们最应该熟悉的部分,没有它上面的语法都不成立。
## 属性自动设置器
对于绑定的任意一个属性(忽略namespace),它会根据自己的属性名去寻找对应的setter方法,而不管对应控件中是否已经声明相同名称的属性字段。例如TextView里面的`android:text`属性,关联的表达式会被设置到`setText()`方法里面。例如我们的图片显示控件ThumbnailDraweeView,虽然没有一个名为`imageURI`的属性,但有`setImageURI`方法,所以我们可以轻松的使用`imageURI`属性去为控件设置要显示的图片:
<com.gzdxjk.healthmall_android_library.widget.facebook.ThumbnailDraweeView
android:id="@+id/image"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="15dp"
app:imageURI="@{item.url}" />
有了这个机制以后,我们可以为任何setter方法在xml中创造对应的属性,而不需要预先在控件中定义,包括事件处理,这也是极大的增加了灵活性。例如:
<com.gzdxjk.healthmall_android_library.widget.LoadingView
android:id="@+id/loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:noDataMessage="@{flowlist.noDataMsg}"
app:onRefreshListener="@{() -> presenter.onStart(loading, context, flowlist)}">
以上示例里面可以看到,我们项目中的LoadingView并没有`noDataMessage`这个属性,也没有`onRefreshListener`这个属性,但通过自动设置器,相应的表达式还是正确的设置到了对应的方法里面。
## 重命名设置器
有的属性对应的setter方法会跟属性名不一致,例如ImageView的`android:tint`属性,它对应的方法是`setImageTintList`,而不是`setTint`。这时候可以使用`@BindingMethods`注解来重命名setter,它是一个类注解:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
## 自定义设置器
假如存在的属性没有对应的setter方法,那么可以编写一个由`@BindingAdapter`注解的静态方法来自定义该属性的行为,例如`android:paddingLeft`属性,并没有一个`setPaddingLeft`方法让自动设置器去设置它对应的表达式,这时候可以:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
假如自动设置器提供的方法不满足你的需求,你也可以使用自定义设置器去覆盖系统提供的方法。
BindingAdapter还可以接收多个参数,对应到一个方法里面。例如我们的流式布局FlowLayout中的自定义设置器:
@BindingAdapter({"bind:child_layout", "bind:items"})
public static void bindTextItem(FlowLayout flowLayout, int childLayoutId, List<String> itemList) {
flowLayout.addTextTag(childLayoutId, itemList);
}
当布局中的`child_layout`和`items`属性同时存在(忽略namespace)并且它们的表达式返回类型和定义好的方法一致时,将会调用`bindTextItem`方法:
<com.gzdxjk.healthmall_android_library.widget.FlowLayout
android:id="@+id/flow_layout"
android:layout_width="match_parent"
android:layout_height="300dp"
app:child_layout="@{@layout/layout_textview}"
app:items="@{flowlist.list}"
app:onTagClickListener="@{(view,tag,position) -> presenter.addListItem(position)}" />
BindingAdapter还允许其处理方法使用变化前的值,具体的格式应该是旧值在前,新值在后:
@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());
}
}
对于一个接口有多个方法的情况,必须为每个方法重写一个接口,并且在自定义设置器中一一对应,由于情况比较特殊,具体实例请查看[官方文档](https://developer.android.com/topic/libraries/data-binding/index.html#custom_setters)。
# 数据类型转换
同样,为了可读性考虑,禁止在xml中使用数据类型转换。