本文建立在有一定使用 DataBinding 经验的基础之上,若还不熟悉 DataBinding 的用法,请参考前一篇博客Data Binding 数据绑定(一)。
在学习 DataBinding 的过程中,参考 Google 官方的 DataBinding 示例 Demo,自己写了一个 DataBindingPractice Demo,用于练手。整个工程采用 MVP 架构 + DataBinding,欢迎 star、fork 和沟通交流。
本文介绍了 DataBinding 一些稍微高级的用法,主要包括以下四部分内容:
- DataBinding 中的数据对象(Data Objects)
- DataBinding 中生成绑定类(Generated Binding)
- DataBinding 中的属性设置(Attribute Setters)
- DataBinding 中的转换器(Converters)
数据对象(Data Objects)
任何普通的 Java 对象(POJO)都可以被 DataBinding 所使用,但是改变 POJO 对象的属性值并不会更新 UI 界面的显示。DataBinding 真正强大之处在于,它可以让你的数据对象具有通知 UI 界面对象的属性已经发生改变的能力。
有三种不同的数据变化通知机制:
- Observable objects
- observable fields
- observable collection
- 如果这其中的一种数据对象被绑定到 UI 界面上,当数据对象的属性值发生变化时,UI 界面会自动更新。
可观察对象(Observable Objects)
- 一个类如果实现了
Observable
接口,那么 DataBinding 则会将一个listener
绑定到该类上,就可以监听该类对象中的属性的变化。Observable
接口具有添加和移除listener
的机制,但是否通知则取决于开发者。 - 为了使开发更容易,DataBinding 提供了一个名为
BaseObservable
的基类,它用于实现listener
注册机制。 - 实现
Observable
的类负责什么时候通知该类的属性发生了变化,只需要在类的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
注解标记过的getter
方法会在BR
class 文件中生成一个入口,BR
class 文件是在 Module 的包下,BR.class
与R.class
的功能类似。
可观察属性(ObservableFields)
- 创建一个
Observable
类还是需要一些工作量的,如果开发者不想花费太多的时间和精力,或者没有太多的属性需要观察监听的话,那么可以使用ObservableField
,或者它的子类:ObservableBoolean
,ObservableByte
,ObservableChar
,ObservableShort
,ObservableInt
,ObservableLong
,ObservableFloat
,ObservableDouble
和ObservableParcelable
。 -
ObservableField
是包含Observable Object
对象的单一字段。原始版本避免了在获取过程中做打包和解包的操作。在数据对象中使用ObservableField
,需要创建一个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 Collections)
- 一些应用会使用动态的结构持有数据,可观察容器类允许使用键值对的形式来存取数据。
- 当键值对中的键是应用型数据(比如:String)时,
ObservableArrayMap
是非常有用的。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中,也可以通过使用 String 类型的键来获取到相应的值。
<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"/>
- 当键值对中的键是 Integer 型的,
ObservableArrayList
则是非常有用的。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局文件中,也可以通过使用 Integer 类型的键来获取到相应的值。
<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"/>
```
## 生成绑定类(Generated Binding)
1. 生成的绑定类通过布局文件中的 Views 和布局文件中的变量联系起来。
2. 如之前所讨论的那样,绑定类的名称和所在的位置都是可以自定义的。
3. 生成的所有的绑定类都是 `ViewDataBinding` 的子类。
### 构建(Creating)
1. 绑定类在 View Inflate 之后立即被创建,以确保在布局中的表达式被绑定到视图之前,View 的层次结构不会被打乱。
2. 有几种方式绑定布局文件,最常用的是使用 Binding 类中的静态方法来绑定类。`inflate` 方法调用一次就可以 Inflate View 并将 View 绑定到 Binding 类上。
3. 还有一个更加简单的方法,只需要一个 `LayoutInflater` 对象和一个 `viewGroup` 对象。
``` Java
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
- 如果布局使用另外不同的机制来 inflate,则可以单独绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
- 有时候,Binding 类的名字不得而知,在这种情况下,则可以使用
DataBindingUtil
生成该 Binding 类:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
带 ID 的 View(Views with IDs)
- 使用 DataBinding 库的布局文件,其中的每个带 ID 的 View,编译以后,都会在该布局文件对应的 Binding 类中生成一个被
public final
修饰的属性,Data Binding 会做一个简单的赋值,在 Binding 类中保存对应 ID 的 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>
在生成的 Binding 类中会有对应的字段:
public final TextView firstName;
public final TextView lastName;
如果使用 DataBinding 库的话,在布局文件中为控件设置
Id
不是必须的,但是在某些情况下,在代码中通过Id
得到控件还是有必要的。
变量(Variables)
布局文件中的每个变量都会生成对应的存取方法,如:
<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>
在该布局文件对应的 Binding 类中,都会生成对应的存取方法,如下所示:
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
- ViewStub 和普通的 View 相比是不一样的。它们最开始是不可见的,当它们被设置为可见的或者调用
inflate
方法时,ViewStub 会被替换为另外一个控件或布局。 - 因为最开始的时候,ViewStub 在布局层级中不可见,Binding 类中对应的控件也应该被移除,以便回收。
- 因为在 Binding 类中,所有 View 对应的属性都是被
final
字段修饰的,所以一个ViewStubProxy
对象代替该 ViewStub,当 ViewStub 被设置为可见的或调用inflate
方法之后,开发者可以通过此代理类ViewStubProxy
得到对应的 ViewStub。 - 当
inflate
一个新的布局时,必须为新的布局创建新的 Binding 类。所以 ViewStubProxy 必须监听 ViewStub 的ViewStub.OnInflateListener
,当 ViewStub 被inflate
的时候,则建立一个新的 Binding 类。 - 因为 ViewStub 只能设置一个
OnInflateListener
,开发者可以为ViewStubProxy
设置一个OnInflateListener
,在 Binding 类被建立以后,OnInflateListener
就会被触发。
代码如下所示:
<layout>
...
<ViewStub
android:id="@+id/viewStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/layout_view_stub"/>
...
</layout>
mBinding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
LayoutViewStubBinding mStubBinding = DataBindingUtil.findBinding(inflated);
mStubBinding.tvViewStub.setOnClickListener((view1 -> showClickToast()));
}
});
高级绑定(Advanced Binding)
动态变量(Dynamic Variables)
- 有时候,一些 Binding 类不为人所知。比如,在
RecyclerView.Adapter
中可以用来处理不同的布局,此时便不知道该 Binding 类具体是什么类型的。而在onBindViewHolder(VH, int)
方法中,ViewHolder 中的 Binding 类又必须被赋值。 - 在这个例子中,所有 RecyclerView 涉及到的布局中,都有一个
item
的变量。 - Adapter 所使用的 ViewHolder 中有一个
getBinding
的方法得到一个ViewDataBinding
的 Binding 类。如下所示:
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立即绑定(Immediate Binding)
当变量或者 observable
变量发生变化时,会在下一帧才触发 Binding,但是有时候需要立即 Binding,可以通过 executePendingBindings()
方法立即触发 Binding。
后台线程
只要不是集合类型的数据,你可以在后台线程中更改数据。Data Binding 会在计算时将每个变量/字段在各个线程中做一份数据拷贝,以避免同步问题。
属性设置(Attribute Setters)
当一个属性值发生变化时,生成的 Binding 类必须调用该控件对应 data binding 表达式的 setter
方法。Data Binding 框架允许自定义调用何种方法改变值。
自动设置属性(Automatic Setters)
- 对于一个属性
attribute
,Data Binding 会尝试着去找setAttribute
方法。属性的命名空间是什么并没有什么关系,只和属性本身的名称有关。例如,为 TextView 的属性android:text
设置了一个 binding 表达式,则 Data Binding 库会去寻找setText(String)
的方法。 - 如果 data binding 表达式返回了一个 int 型数据,Data Binding 则会去寻找
setText(int)
的方法。对于 data binding 表达式的返回值一定要小心处理,如果必要的话,需要做类型强制装换。 - 需要注意的是,就算给定名称的属性不存在,Data Binding也会生效。正是因为如此,使用 Data Binding 则可以方便地自定义属性。例如,
DrawerLayout
控件并没有什么属性,但是却有很多的setters
方法,就可以方便地使用自动设置属性给DrawerLayout
设置属性。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
重命名属性设置(Renamed Setters)
有些属性有其对应的 setter
方法,但是该 setter
方法和其属性名称并不是那么相匹配。对于这些方法,可以使用 BindingMethods
注解将该属性与对应的方法关联起来。例如:属性 android:tint
真正是和 setImageTintList(ColorStateList)
关联起来的,而不是和 setTint
方法关联:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
在 Android 框架中实现的属性的 setter
方法已经不错,所以不需要开发者重命名属性设置了。
自定义属性设置(Custom Setters)
- 一些属性需要自定义逻辑。例如,没有一个
setter
方法和属性android:paddingLeft
相关联,但是却存在setPadding(left, top, right, bottom)
方法。被BindingAdapter
注解修饰的静态 binding adapter 方法允许开发者自定义一个属性的setter
方法如何被调用。
Android 已经内置了一些BindingAdapters
。如下是一个与属性paddingLeft
相关联的setter
方法。
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
- Binding adapters 在其他自定义类型上也非常好用。
当开发者自定义的binding adapters
与默认的adapters
冲突时,开发者自定义的会覆盖默认的。
当然也可以自定义接收多个参数的adapters
,一个在非主线程中加载图片的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}"/>
```
如果在一个 ImageView 中 `imageUrl` 属性和 `error` 属性同时被使用,并且 `imageUrl` 是 String 类型的,`error` 属性是 Drawable 类型的,则这个 `adapter` 将会被调用。
* 在匹配的过程中,自定义的命名空间将会被忽略
* 也可以为 android 命名空间编写 adapter
3. `binding adapter` 中的方法可以获取旧值,只需要将旧值放置在前,而新值放置在后,如下所示:
``` Java
@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());
}
}
- 事件处理
handlers
中,只可用于只拥有一个抽象方法的接口或抽象类,如下所示:
@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);
}
}
}
- 当一个
listener
中有多个方法时,它必须拆分成多个listener
。例如: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);
}
因为改变一个 listener
必将会影响到另一个,所以我们必须有三个不同binding adapters
,包括修改一个属性和修改两个属性的,如下所示:
@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
,而不是通过setter
方法设置监听器的。android.databinding.adapters.ListenerUtil
可以用来跟踪之前的listener
,并可以在Binding Adaper
中移除监听器listener
。
通过向OnViewDetachedFromWindow
和OnViewAttachedToWindow
接口添加@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
注解,Data Binding 代码生成器知道监听器只在 Honeycomb MR1 设备或更新版本的设备中使用。
转换器(Converters)
对象转换(Object Conversions)
当 binding 表达式返回一个对象时,一个 setter
方法(自动 Setter,重命名 Setter,自定义 Setter),并将返回的对象强制转换成所选择的 setter
方法所需要的类型。
以下是一个使用 ObservableMaps
持有数据并转换的例子:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap
返回一个对象,并且这个对象会被自动地转换为 setter setText(CharSequence)
所需要的类型。当参数类型选择存在疑惑时,需要开发者手动地将数据类型进行转换。
自定义类型转换器(Custom Conversions)
有时候,属性的值需要在特定类型之间自动转换。例如,在设置背景的时候:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
在这里,背景需要 Drawable 类型的,但是颜色却是 Integer 类型的。当需要一个 Drawable,binding 表达式返回的却是 Integer 的,所以此 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"/>
DataBinding 第二篇文章也介绍完成。至此,关于 DataBinding 的文章到此就告一段落。如果有什么问题欢迎指出。我的工作邮箱:jiankunli24@gmail.com
参考资料:
深入Android Data Binding(一):使用详解 -- YamLee
Android Data Binding 系列(一) -- 详细介绍与使用 -- ConnorLin
DataBinding(一)-初识 -- sakasa
(译)Data Binding 指南 -- 杨辉