作者: weiyf
时间: 2016-11-14 16:19:28

Data Binding系列提纲概况:

  1. Data Binding —— 快速入门
  2. Data Binding —— 基础用法

上一篇我们说到Data Binding的快速入门,编写了一个简单的sample,但是对于它的含义我们还是似懂非懂,这篇博客我们来学一下Data Binding的基础用法。

文章中有些方法或者变量是没有实际意义的,只是为了更好的说明。

数据对象

假设我们有这样的一个数据对象:

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
public class User {
private String firstName;

private String lastName;

public int age;

public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User {
public String firstName;

public String lastName;

public int age;

public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}

对于Data Binding来说,这两个类是完全相同的,在上一篇的例子中,TextView的android:text属性中的@{user.firstName}表达式会去读取Java对象的firstName字段或者getFirstName()方法。如果firstName()方法存在也可行。

绑定数据

引入布局

上篇我们已经说明了Data Binding的命名生成规则,生成的绑定类包含了所有关于这个布局中的views的属性,而且它还知道如何给绑定表达式分配所属的值。本篇尚不去讨论它是如何实现的,关于Data Binding的原理,我们后续会作讨论。

没有使用Data Binding之前我们是这样引入布局的。

1
2
3
4
5
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(this, R.layout.main_activity);
}

使用了Data Binding之后我们需要使用到一个名为DataBindingUtil的类来引入布局,并绑定数据:

1
2
3
4
5
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
}

也可以这样引入布局:

1
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());

如果你在ListView或者RecycleView使用Data Binding,你可以这样引入布局:

1
2
3
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

实例化控件

Data Binding会自动帮我们实例化布局中的控件,不需要我们手动去调用findViewById()方法进行实例化,所有带id的view我们都可以通过binding实例对它进行访问。

变量绑定

使用data标签,我们可以在xml上绑定我们需要的变量,可以在java代码中通过binding实例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
<layout>

<data>

<variable
name="user"
type="cn.weiyf.databindingsample.gettingStarted.entity.User"/>

</data>

<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

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

<TextView
android:text="@{user.lastName}"
android:id="@+id/lastName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
1
2
User user = new User("firstName", "lastName", 16);
binding.setUser(user);

或者

1
binding.setVariable(BR.user, user);

事件处理

Data Binding可以在xml上编写表达式来绑定一些view调度的事件。例如:

  • android:onClick
  • android:onLongClick
  • android:onTextChanged

方法引用

事件可以直接绑定在处理方法上,就像android:onClick的方式可以分配给一个Activity中的一个方法。相比与View#onClick属性有一个主要的优点,就是表达式是在编译期进行处理的,所以如果那个方法不存在或者它的写法存在问题时,会在编译期出现编译时错误。方法签名需要和对应的listener方法一致。

监听绑定

监听绑定实质上就是方法引用,只不过监听绑定可以允许你运行任意的Data Binding表达式,不需要受特定的listener签名限制,这个特性是在Android Gradle插件2.0版本之后才可用。在方法引用中,方法参数必须与时间监听器的参数相匹配。但是在监听绑定中,只需要返回的值与监听器的表达式返回的预期的值相匹配就可以(除非预期返回的是void)。

方法引用和监听器绑定两者之间的主要不同是实际的监听器实现是创建在数据已经绑定之后,不是在事件被调用的时候。

为了更好的演示,下面的例子使用了双向绑定,关于双向绑定,下篇介绍Data Binding高级用法时再做详细介绍,将User修改为:

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
39
40
41
42
43
44
45
46
47
48
public class User extends BaseObservable {


private String firstName;

private String lastName;

private int age;

public User() {
}

public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}


@Bindable
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}

@Bindable
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}

@Bindable
public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<layout>

<data>

<variable
name="user"
type="cn.weiyf.databindingsample.entity.User"/>

<variable
name="presenter"
type="cn.weiyf.databindingsample.ui.fragment.BasicUsageFragment.Presenter"/>
</data>


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!--方法引用-->

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onTextChanged="@{presenter.onFirstNameChanged}"
android:text="@={user.firstName}"/>

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={user.lastName}"/>

<!--方法引用-->

<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{presenter.changedFirstName}"
android:text="点击改变User的firstName"/>

<!--监听器绑定-->

<TextView
android:onClick="@{() -> presenter.printUser(user)}"
android:id="@+id/watcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>

</LinearLayout>

</layout>
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
private Presenter mPresenter;

@Override
protected void initViews(@Nullable Bundle bundle) {
mPresenter = new Presenter();
mPresenter.mUser = new User("firstName", "lastName", 16);
mBasicUsageBinding.setUser(mPresenter.mUser);
mBasicUsageBinding.setPresenter(mPresenter);
}

public class Presenter {

private User mUser;

public void onFirstNameChanged(CharSequence s, int start, int before, int count) {
mBasicUsageBinding.watcher.setText("firstName: " + s + "\nstart: " + start +
"\nbefore: " + before + "\ncount: " + count);
}

public void changedFirstName(View view) {
mUser.setFirstName("改名字怎么了");
showToast("点击事件, \nuser: " + mBasicUsageBinding.getUser().toString());
}

public void printUser(User user) {
showToast(user.toString());
}

}

避免复杂的监听器

监听器表达式是非常强大的并且可以使你的代码更容易阅读。在另一方面,监听器包含复杂的表达式会导致你的布局难以去阅读和变得不可维护。这些表达式需要和将可用数据从UI传递到回调方法那样简单。您应该在从侦听器表达式调用的回调方法中实现任何业务逻辑。

存在一些专门的点击事件处理,并且他们需要一个android:onClick以外的属性来避免冲突。已创建以下属性以避免此类冲突:

Listener Setter 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

布局细节

导入

有些时候我们需要使用某些类中的变量或者静态方法,我们可以像java一下在data标签中使用零个或者多个import标签,就可以让你像在java一样轻松引用类。

1
2
3
4
5
6
7
8
9
<data>
<import type="android.view.View"/>
</data>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{(user.firstName.length() == 0 || user.lastName.length() == 0) ? View.INVISIBLE : View.VISIBLE}"
/>

当导入的包类名冲突时,我们可以使用import标签的alias属性。

1
2
3
4
5
6
7
<import
alias="User1"
type="cn.weiyf.databindingsample.entity.User"/>

<import
alias="User2"
type="cn.weiyf.databindingsample.utils.entity.User"/>

当然,导包之后我们就可以直接使用这个类了,声明变量的时候不需要写完整包名:

1
2
3
4
5
6
7
<variable
name="user1"
type="User1"/>

<variable
name="user2"
type="User2"/>

导包之后我们可以使用类的静态变量或静态方法

1
2
3
4
5
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{CommonUtils.toUpperCase(user.toString())}"
/>

需要注意的是,java.lang.*是自动导入的,就像在Java中一样。

变量

其实,我们已经在无形中学会使用变量了,在data标签内部可以使用任意数量的variable标签来声明将会在layout中的表达式中使用的变量。

1
2
3
4
5
6
7
<variable
name="user"
type="cn.weiyf.databindingsample.entity.User"/>

<variable
name="presenter"
type="cn.weiyf.databindingsample.ui.fragment.BasicUsageFragment.Presenter"/>

在编译期会检查变量的类型,所以如果一个变量实现了Observable或者是一个Observable集合,那么应该反应在类型中。如果这个变量是一个没有实现Observable*接口的基类或者接口,这个变量将不会被observed。

当针对不同的配置有不同的布局文件的时候(e.g.横屏和竖屏),这些变量会被合并。所以这些布局文件之间不能声明有冲突的变量。

生成的绑定类中每一个声明的变量会有一个setter和getter。这些变量会使用java的默认值直到它的setter被调用 —— 引用类型为nullint为0,booleanfalse,如此类推。

还会生成一个名为context的特殊变量,以根据需要用于绑定表达式。context的值是来自根视图的getContext方法返回的Context.contenxt变量会被同名的显示变量覆盖。

自定义Binding类名

这没什么实际意义,就改个名字或者改变一下生成绑定类的包名,详细请看我翻译的Data Binding指南

Includes

当我们使用include标签来引入布局时,我们也可以将某些变量传入到include的布局:

被include的布局中必须也要声明一个相同的变量。

1
2
3
<include
layout="@layout/view_basic_usage_inner"
bind:user="@{user}"/>
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="cn.weiyf.databindingsample.entity.User"/>
</data>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下面是include的layout"/>


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.toString()}"/>

</LinearLayout>
</layout>

Data binding 不支持include一个直接使用merge标签的布局。举个例子:下面这个布局是不支持的:

1
2
3
4
5
6
7
8
9
10
11
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="user" type="cn.weiyf.databindingsample.entity.User"/>
</data>
<merge>
<include
layout="@layout/view_basic_usage_inner"
bind:user="@{user}"/>
</merge>
</layout>

在编译时会报错:

1
Error:Data binding does not support include elements as direct children of a merge element.

表达式语言

  • 数学计算(Mathematical) + - / * %
  • 字符串连接(String concatenation) +
  • 逻辑(Logical) && ||
  • 二进制(Binary) & | ^
  • 一元(Unary) + - ! ~
  • 位移(Shift) >> >>> <<
  • 比较(Comparison) == > < >= <=
  • instanceof
  • 分组(Grouping) ()
  • 文字-字符(Literals - character),字符串(String),数字(numeric),null
  • 类型转换(Cast)
  • 方法调用(Method calls)
  • 域读取(Field access)
  • 数组读取(Array access) []
  • 三元(Ternary operator) ?:

这部分比较简单,举个常用的例子:

1
2
3
4
5
6
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"1 + 1 = " + String.valueOf(1 + 1)}'
android:visibility="@{(user.firstName.length() == 0 || user.lastName.length() == 0) ? View.INVISIBLE : View.VISIBLE}"
/>

当表达式中需要用到双引号“”时,可以在最外层使用单引号’’

有一些在java中常用的表达式语法在Data Binding是不可用的,例如:

  • this
  • super
  • new
  • 显示泛型调用

空合并操作符

我们有时候会遇到当一个字段为空的时候显示另外一个字段这样的需求,Data Binding可以轻松做到:

1
android:text="@{user.firstName ?? user.lastName}"

其实就相当于:

1
android:text="@{user.firstName != null ? user.displayName : user.lastName}"

避免空指针

生成的Data binding代码自动的检查空和避免空指针异常。举个例子,在@{user.name}表达式中,如果user是nulluser.name会被制定它的默认值null。如果你是引用user.age,age是一个int类型,所以它是默认值0;

1
2
3
4
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user1.age)}"/>

集合

常用集合:arrays,lists,sparse lists, 可以使用[]运算符来访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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]}"

资源

Data Binding对资源的使用也是比较灵活的,例如,我们可以根据变量来确定padding的大小:

1
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

也可以进行字符串合并或者格式化

1
<string name="nameFormat">%s, %s</string>
1
android:text="@{@string/nameFormat(firstName, lastName)}"

示例代码

本系列博客代码示例

展望

关于Data Binding的基础用法还有很多细节,因为篇幅的原因,无法全部展示,以后会在代码实践中慢慢体现其细节,下一篇我将会介绍Data Binding的进阶用法,包括动态绑定,双向绑定,自定义属性, Lambda表达式等,敬请期待。