DataBinding 简单运用

Data Binding Library 从 2015 Google I/O 上发布到至今,已经有一年多的长足发展,目前在 Android Studio2.2 版本上已经拥有比较成熟的使用体验。可以说 Data Binding 已经是一个可用度较高,也能带来实际生产力提升的技术了。

编译环境

2.0 版本以后的 Android Studio 已经内置支持了 DataBinding ,我们只要在 gradle 文件中添加如下代码就可以使用 Databinding:

1
2
3
4
5
6
android {
....
dataBinding {
enabled = true
}
}

xml 文件的处理

1
2
3
4
5
<layout>
<data class = "CustomBinding">
</data>
// 原来的layout
</layout>

layout标签位于布局文件最外层,可以使原来的普通布局文件转化为 databinding layout ,同时会在build/ganerated/source/apt下相关目录下生成 ***Binding 类

默认生成规则:xml通过文件名生成,使用下划线分割大小写,即 activity_main.xml 会生成对应的 ActivityMainBinding

data标签用于申明 xml 文件中的变量用于绑定 View,可以通过对标签的修饰来指定生成 Binding 类的自定义名称,如上述的布局文件最终会生成一个 CustomBinding 类

Java 代码的处理
需要用 DataBindingUtil 类中的相关方法取代原先的 setContentView 及 inflate 获得 ***Binding 实例类

取代findViewById方法

findViewById(int id) 方法是将 View 的实例与 xml 布局文件中的 View 对应赋值的过程,需要遍历所有的 childrenView 查找。更关键的一点是如果比较复杂的页面,可能会存在数十个控件,光写 findViewById 也会让人挺崩溃的。虽说有着诸如 ButterKnife 这样优秀的第三方库,但使用数据绑定方式无疑更简洁明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private TextView mFirstNameTv;
private TextView mLastNameTv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(this, R.layout.activity_first);
mFirstNameTv = (TextView) findViewById(R.id.tv_first_name);
mLastNameTv = (TextView) findViewById(R.id.tv_last_name);
}

//********* 或者使用 *********

private ActivityFirstBinding mFirstBinding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在mBinding中有布局文件中带id的View变量
mFirstBinding = DataBindingUtil.setContentView(this, R.layout.activity_first);
}

采用 DateBinding 后,所有的 View 会在 Binding 实例类生成对应的实例,而有 id 的 View 则会使用 public 进行修饰,而变量名的生成规则是通过下划线分割大小写,即 id = "@+id/main_view" 会生成对应的 mainView 的变量,我们可以直接通过 binding.mainView 获取,直接节省了在 activity 中声明一长串变量的步骤,也不需要再写 findViewById 方法或者加上 @BindView 的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<layout
xmlns:android="http://schemas.android.com/apk/res/android">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/tv_first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>

在 activity_first.xml 布局文件中添加 databindind 的 layout 标签后会生成 ActivityFirstBinding 类

1
2
3
4
// views
private final android.widget.LinearLayout mboundView0;
public final android.widget.TextView tvFirstName;
public final android.widget.TextView tvLastName;

带 id 的 view 最终会生成 public final 修饰的字段,而不带 id 的 view 也会生成 private final 修饰的字段。而这些则是在 ActivityLoginBinding 的构造函数中赋值的,仅仅只需要遍历一遍整个的 view 树,而不是多个 findViewById 方法遍历多次

为布局文件绑定Variable

数据绑定getter和setter

Variable 是 DataBinding 中的变量,可以在data标签中添加variable标签从而在 xml 中引入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<layout>
<data>
<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>
</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>

<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@{user.lastName}"/>

</LinearLayout>

</layout>

variable 就是普通的 POJO 类,实现 getter 方法,并没有提供更新数据刷新 UI 的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static class User {

private String firstName;
private 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;
}
}

如果希望数据变更后 UI 会即时刷新,就需要继承 Observable 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}
}

BaseObservable 提供了 notifyChange 和 notifyPropertyChanged 两个方法来刷新 UI ,前者刷新所有的值,而后者则是刷新 BR 类中有标记的属性,而 BR 类中的标记生成需要用Bindable的标签修饰对应的 getter 方法
同时 databinding 提供了 Observable** 开头的一系列基础类可以避免继承 BaseObservable

1
2
3
4
5
6
7
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** 也是通过继承 BaseObservable 实现的,调用set方法时会调用 BaseObservable 的 notifyChange 方法

1
2
3
4
5
6
7
8
9
10
11
user.firstName.set("first");
String lastName = user.lastName.get();

//********************************

public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}

运算表达式

运算符
支持绝大部分的 Java 写法,允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式

  • 算术 + - * / %
  • 字符串合并 +
  • 逻辑 && ||
  • 二元 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <=
  • instanceof
  • Grouping ()
  • 文字 - character, String, numeric, null
  • Cast
  • 方法调用
  • Field 访问
  • Array 访问 []
  • 三目运算符 ?:

尚且不支持 this,super,new 以及显式的泛型调用

空指针处理
无需判断对象是否为 null,DataBinding 会自动检查是否为 null,如果引用对象为 null,那么所有属性引用的都是 null 的,你无需判断也不会导致崩溃

空合并运算符 ??
引用的对象为 null,需要做额外的判断,DataBinding 提供了空合并运算

1
2
3
android:text="@{user.firstName ?? user.lastName}"
//会取第一个非空值作为结果,相当于
android:text="@{user.firstName != null ? user.firstName : user.lastName}"

集合数组的调用
对于数组,List,Map,SparseArray的访问,我们可以直接通过[]的数组下标来访问,值得注意的是数组越界的问题

资源文件的引用
值得一说的是可以直接组合字符串

1
2
3
android:text="@{@string/nameFormat(firstName, lastName)}"

<string name="nameFormat">%s, %s</string>

也可以对数值类应用直接进行运算

1
android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"

需要注意的是一些资源文件需要确切的名称

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

属性关联
DataBinding 库通过解析 View 的 setter 方法来完成赋值过程,android:text = "@user.firstName"就相关于调用了
TextView 的 tv.setText(user.firstName)

甚至可以调用 View 未提供的布局属性,只要 View 提供了对应的 setter 方法。
举个例子:

1
2
3
4
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"/>

DrawerLayout 有个 setScrimColor(int color)方法,所以可以在布局中使用未定义的app:scrimColor属性,通过 app 命名空间修饰的属性会自动关联到对应的方法

属性扩展

BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局属性并没有完整对应的方法提供,比如说 ImageView 的"android:tint"布局属性的对应方法是setImageTintList(@Nullable ColorStateList tint),这时就需要使用 DataBinding 提供的处理方法,使用BindingMethods注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@BindingMethods({
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, String imageUri) {
if (imageUri == null) {
view.setImageURI(null);
} else {
view.setImageURI(Uri.parse(imageUri));
}
}

@BindingAdapter("android:src")
public static void setImageUri(ImageView view, Uri imageUri) {
view.setImageURI(imageUri);
}

@BindingAdapter("android:src")
public static void setImageDrawable(ImageView view, Drawable drawable) {
view.setImageDrawable(drawable);
}
}

这是系统提供的 ImageViewBindingAdapter,可以在引入了 DataBinding 后全局搜索查看详情,通过BindingMethod注解将两者关联起来,但是如果 View 甚至没有实现对应方法或者需要绑定自定义方法,这是可以使用BindingAdapter注解

BindingConversion 注解
有时在 xml 中绑定的属性,未必是最后的set方法需要的,比如想用color(int),但是 view 需要 Drawable,比如我们想用String,而 view 需要的是 Url 。这时候就可以使用BindingConversion注解

1
2
3
4
<View
android:background=“@{isError ? @color/red : @color/white}”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”/>
1
2
3
4
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}

链式表达式

1
2
3
<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

代码可以优化成

1
2
3
4
<ImageView android:id=“@+id/avatar”
android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>

在系统生成的 Bindinng 类中,会被解析成这三个控件可见性都跟随着 user.isAdult 的状态而改变

使用Callback

事件绑定

DataBinding 不仅可以在布局文件中为控件绑定数值,也可以在布局文件中为控件绑定监听事件

  • android:onClick
  • android:onLongClick
  • android:onTouch
  • ……

通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<layout
xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<import type="android.view.View"/>

<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>

<variable
name="presenter"
type="com.sanousun.sh.databinding.activity.SecondActivity.Presenter"/>

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<TextView
android:id="@+id/tv_mobile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{presenter::mobileClick}"
android:text="@{user.firstName}"/>

<TextView
android:id="@+id/tv_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->presenter.pwdClick()}"
android:text="@{user.lastName}"/>

</LinearLayout>

</layout>

在java代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Presenter {
public void mobileClick(View view) {
Toast.makeText(SecondActivity.this, "mobile click", Toast.LENGTH_LONG).show();
}

public void pwdClick() {
Toast.makeText(SecondActivity.this, "pwd click", Toast.LENGTH_LONG).show();
}
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second);
mSecondBinding.setUser(new User("da", "shu"));
mSecondBinding.setPresenter(new Presenter());
}

事件绑定使用 lambda 表达式,绑定形式主要是有两种形式:

Method References

需要方法参数及返回值与对应的 listener 一致,在编译时生成对应的 listenerImpl 并在放置 presenter 时为对应控件添加监听,如上面的 mobileClick

1
2
3
4
5
6
7
8
9
10
11
12
13
// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
private com.sanousun.sh.databinding.activity.SecondActivity.Presenter value;
public OnClickListenerImpl setValue(com.sanousun.sh.databinding.activity.SecondActivity.Presenter value) {
this.value = value;
return value == null ? null : this;
}

@Override
public void onClick(android.view.View arg0) {
this.value.mobileClick(arg0);
}
}

代码中会做 presenter 的空判断

Listener Bindings

无需匹配对应 listener 的参数,只需要保证返回值的一致即可(除非是void)。与 Method References 的最大的不同点在于
它是在点击事件发生时相应的

1
2
3
4
5
6
7
8
9
10
11
12
13
// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
// localize variables for thread safety
// presenter
com.sanousun.sh.databinding.activity.SecondActivity.Presenter presenter = mPresenter;
// presenter != null
boolean presenterObjectnull = false;

presenterObjectnull = (presenter) != (null);
if (presenterObjectnull) {
presenter.pwdClick();
}
}

这个方法会在页面有点击时间时调用,同样也会做空判断

当然你也可以通过@BindingMethods@BindingAdapter进行自定义的扩展

双向绑定

有别于单向绑定使用的@{}符号,双向绑定使用@={}符号用于区别,目前支持的属性有 text,checked,year,month,day,hour,rating,progress 等

InverseBindingListener

实现双向绑定需要归功于 DataBinding 库中的 InverseBindingListener 接口,这个监听器的作用是监听目标控件的属性改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of user.male
// is user.setMale((boolean) callbackArg_0)
boolean callbackArg_0 = mboundView1.isChecked();
// localize variables for thread safety
// user.male
boolean maleUser = false;
// user
com.sanousun.sh.databinding.bean.User user = mUser;
// user != null
boolean userObjectnull = false;
userObjectnull = (user) != (null);
if (userObjectnull) {
user.setMale((boolean) (callbackArg_0));
}
}
};

对应 DataBinding 类中有根据双向绑定生成的 Inverse Binding Event Handlers

1
2
3
4
5
@Override
protected void executeBindings() {
......
android.databinding.adapters.CompoundButtonBindingAdapter.setListeners(this.mboundView1, (android.widget.CompoundButton.OnCheckedChangeListener)null, mboundView1androidCh);
}

在绑定时,设置到对应的控件中,当监听控件属性改变时,就会触发重绑定,更新属性值

InverseBindingMethods 和 InverseBindingAdapter 注解

如果你想做自定义的双向绑定,你必须充分理解这几个注解的含义。

1
2
3
4
5
6
7
@Target({ElementType.ANNOTATION_TYPE})
public @interface InverseBindingMethod {
Class type();
String attribute();
String event() default ""; // 默认会根据attribute name获取get
String method() default "";// 默认根据attribute增加AttrChanged
}

以系统定义的 CompoundButtonBindingAdapter 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@BindingMethods({
@BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
@BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
@InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {

@BindingAdapter("android:checked")
public static void setChecked(CompoundButton view, boolean checked) {
if (view.isChecked() != checked) {
view.setChecked(checked);
}
}

@BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
requireAll = false)
public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
final InverseBindingListener attrChange) {
if (attrChange == null) {
view.setOnCheckedChangeListener(listener);
} else {
view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (listener != null) {
listener.onCheckedChanged(buttonView, isChecked);
}
attrChange.onChange();
}
});
}
}
}

双向绑定需要为属性绑定一个监听器,这里就是需要为"android:checked"属性绑定监听器,通过 @InverseBindingMethod(type = CompoundButton.class, attribute = “android:checked”),databinding 可以通过 checkedAttrChanged 找到 OnCheckedChangeListener,设置 OnCheckedChangeListener 来通知系统生成的 InverseBindingListener 调用 onChange 方法,从而通过 getter 方法来获取值。值得注意的是为了防止无限循环调用,setter 方法必须要去进行重判断

同样如果没有对应方法,可以自定义 InverseBindingAdapter 来实现,详情见系统TextViewBindingAdapter

隐式调用

实现了双向绑定的属性就可以隐式调用,而不用写繁琐的 listener

1
2
<CheckBox android:id="@+id/cb"/>
<ImageView android:visibility="@{cb.checked ? View.VISIBLE : View.GONE}"/>

属性改变监听

当然我们可以通过 Observable.OnPropertyChangedCallback 来监听属性的改变,从而实现具体的业务逻辑

1
2
3
4
5
6
7
8
user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
if (i== BR.firstName){
Toast.makeText(ThirdActivity.this, user.getFirstName(), Toast.LENGTH_LONG).show();
}
}
});

RecyclerView的处理

只要简单的定义 ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {

protected final T mBinding;

public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}

public T getBinding() {
return mBinding;
}
}

因为逻辑和属性的绑定在xml中就已经处理好,adapter 的创建变得十分的容易,一般情况下可以直接使用,如果需要额外的更改可以继承。而点击事件的监听可以在 onBindViewHolder 中设置
对于含有多种 viewType 的列表适配器,在不同 xml 布局文件中 variable 的 name 可以全部写为 item,那么在绑定数据时
无需特殊处理

1
2
3
4
5
6
@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
final Data data = mData.get(position);
holder.getBinding().setVariable(item, data);
holder.getBinding().executePendingBindings();
}

在生成的代码中会去检查它的类型,并将其赋值

高级用法

component 注入

Data Binding Component详解 - 换肤什么的只是它的一个小应用!

原理简述

解析

编译时,系统会将 xml 文件拆分为两部分,数据部分的 xml 和布局部分的 xml,分别存放于app/build/intermediates/data-binding-infoapp/build/intermediates/data-binding-layout-out之中,数据部分的 xml 文件记录 view 对应的赋值表达式,而布局部分的 xml 则是普通的布局如下

1
2
3
4
5
<Button
android:id="@+id/btn_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:tag="binding_1"/>

特殊在于每个控件都会生成 tag,作用是生成 DataBinding 时可以绑定对应控件,因此在布局文件中需要避免书写tag
解析xml -> 解析表达式 -> java编译 —> 解析依赖 -> setter

1
2
3
4
5
6
7
8
9
10
11
public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 1);
final Object[] bindings = mapBindings(bindingComponent, root, 4, sIncludes, sViewsWithIds);
this.activityMain = (android.widget.LinearLayout) bindings[0];
this.activityMain.setTag(null);
this.btnBtn = (android.widget.Button) bindings[1];
this.btnBtn.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}

在生成的 binding 类中,构造函数会为所有的控件赋值,此时会将 tag 值去除,所以说为 View 的赋值需要在获取 DataBinding 实例之后。初始化时遍历 view 赋值比 findViewById 效率高得多

绑定

绑定的代码都在生成的 DataBinding 类中的 executeBindings 方法中,不管任何涉及到更新 ui 的地方最终都会调用这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
//一些变量的定义
......

if ((dirtyFlags & 0x5L) != 0) {
//根据flag的值判断是否需要做相应的改变
......
}
......
}

databinding 使用位标记来检验更新(dirtyFlags),每一个标志位都有自己的含义,生成的规则由内部解析表达式后确定,在ViewDataBinding 中我们可以看到

1
2
3
4
5
6
7
8
9
10
11
12
if (USE_CHOREOGRAPHER) {
mChoreographer = Choreographer.getInstance();
mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mRebindRunnable.run();
}
};
} else {
mFrameCallback = null;
mUIThreadHandler = new Handler(Looper.myLooper());
}

批量刷新会发生在系统的帧布局刷新时,系统帧布局刷新回调 -> mRebindRunnable -> executePendingBindings -> executeBindings,此时才会触发数据更改的操作

更新

刷新布局最终都会调用 executeBindings 方法,而在父类 ViewDataBinding 类是由 executePendingBindings 调用方法,我们可以直接调用此方法来加载挂起的属性变更,而不用等待下一次的帧布局刷新
而所有的 Variable 内部属性的改变则会注册监听器,监听改变 -> handleFieldChange -> requestRebind -> executePendingBindings -> executeBindings 最终改变属性

参考

从零开始的Android新项目7 - Data Binding入门篇
棉花糖给 Android 带来的 Data Bindings(数据绑定库)