这篇文章介绍了如何使用Data Binding库来写声明的layouts文件,并且用最少的代码来绑定你的app逻辑和layouts文件。
Data Binding库不仅灵活而且广泛兼容- 它是一个support库,因此你可以在所有的Android平台最低能到Android 2.1(API等级7+)上使用它。
需求:Android Studio 1.3.0-beta1 或 更高版本。
请注意:Data Binding库当前是测试版本。在Data Binding处于测试阶段时,开发者应该了解以下注意事项:
- 这是旨在生成开发者的反馈功能的测试版本。它可能包含bugs,或者不适合你的使用案例,所以需要您在使用它时自担风险。即便如此,我们非常希望得到您的反馈!使用issue tracker来让我们知道对于你的使用案例什么可以工作或者不能工作。
- Data Binding库的测试版本倾向于显著的改变,包括那些不是与您的应用程序兼容的源代码。即,在未来可能会进行大量的返工来更新此库。
- 虽然伴有标准Android SDK和Google Play服务条款适用警告,开发人员可以随时发布内置了与Data Binding库beta版本的应用程序。而且采用新的库或工具是一个相当好的主意来彻底测试你的应用程序。
- 我们在这个时候才刚刚开始与Android Studio的支持。未来会有进一步的Android Studio的支持。
- 通过使用Data Binding库beta版本,你承认这些警告。
要开始使用Data Binding,首先需要在Android SDK Manager的支持库里下载该库。
请确保您使用的是Android Studio的兼容版本。Android Studio的Data Binding插件需要Android Studio 1.3.0-beta1或 更高版本。
你的app要使用Data Binding,需要添加Data Binding到gradle构建文件里,如下:
1
2
3
4
5
|
dependencies {
classpath
"com.android.tools.build:gradle:1.2.3"
classpath
"com.android.databinding:dataBinder:1.0-rc0"
}
}
|
然后确保jcenter在repositories列表里,如下:
1
2
3
4
5
|
allprojects {
repositories {
jcenter()
}
}
|
在每一个你想要使用Data Binding的module,添加如下的插件:
1
2
|
apply plugin: ‘com.android.application'
apply plugin:
'com.android.databinding'
|
Data Binding插件将会在你的项目内添加必需提供的以及编译配置依赖。
Data Binding layout文件有点不同的是:起始根标签是layout,接下来一个data元素以及一个view的根元素。这个view元素就是你没有使用Data Binding的layout文件的根元素。举例说明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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内描述了一个名为user的变量属性,使其可以在这个layout中使用:
1
|
<variable name=
"user"
type=
"com.example.User"
/>
|
在layout的属性表达式写作@{},下面是一个TextView的text设置为user的firstName属性:
1
2
3
|
<TextView android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:text=
"@{user.firstName}"
/>
|
假设你有一个user的plain-old Java Object(POJO):
1
2
3
4
5
6
7
8
|
public
class
User {
public
final
String firstName;
public
final
String lastName;
public
User(String firstName, String lastName) {
this
.firstName = firstName;
this
.lastName = lastName;
}
}
|
这个类型的对象拥有从不改变的数据。在app中它是常见的,可以读取一次并且之后从不改变。当然也可以使用JavaBeans对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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;
}
}
|
从Data Binding的角度来看,这两个类是等价的。用于TextView中的android:text属性的表达式@{user.firstName}将访问前者POJO对象中的firstName和后者JavaBeans对象中的getFirstName()方法。
默认情况下,一个Binding类会基于layout文件的名称而产生,将其转换为Pascal case(译注:首字母大写的命名规范)并且添加“Binding”后缀。上述的layout文件是activity_main.xml,因此生成的类名是ActivityMainBinding。此类包含从layout属性到layout的Views中所有的bindings(例如user变量),并且它还知道如何给Binding表达式分配数值。创建bindings的最简单的方式是在inflating(译注:layout文件与Activity/Fragment的“链接”)期间如下:
1
2
3
4
5
6
7
|
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(
this
, R.layout.main_activity);
User user =
new
User(
"Test"
,
"User"
);
binding.setUser(user);
}
|
就是这样,运行app后,你将会看到Test User。或者你可以通过如下获取View:
1
|
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
|
如果你在ListView或者RecyclerView adapter使用Data Binding时,你可能会使用:
1
2
3
4
|
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup,
false
);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup,
false
);
|
零个或多个import元素可能在data元素中使用。这些只用在你的layout文件中添加引用,就像在Java中:
1
2
3
|
<data>
<
import
type=
"android.view.View"
/>
</data>
|
现在,View可以使用你的Binding表达式:
1
2
3
4
5
|
<TextView
android:text=
"@{user.lastName}"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:visibility=
"@{user.isAdult ? View.VISIBLE : View.GONE}"
/>
|
当类名有冲突时,其中一个类名可以重命名为alias::
1
2
3
|
<
import
type=
"android.view.View"
/>
<
import
type=
"com.example.real.estate.View"
alias=
"Vista"
/>
|
这样,在该layout文件中Vista对应com.example.real.estate.View,而View对应android.view.View。导入的类型可以在Variable和表达式中使用作为引用来使用:
1
2
3
4
5
6
|
<data>
<
import
type=
"com.example.User"
/>
<
import
type=
"java.util.List"
/>
<variable name=
"user"
type=
"User"
/>
<variable name=
"userList"
type=
"List<User>"
/>
</data>
|
注意:Android Studio还没有处理imports,所以自动导入Variable在你的IDE不能使用。您的app仍会正常编译,你可以在您的Variable定义中使用完全符合规定的名称来解决该IDE问题。
1
2
3
4
|
<TextView
android:text=
"@{((User)(user.connection)).lastName}"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
/>
|
导入的类型还可以在表达式中使用static属性和方法:
1
2
3
4
5
6
7
8
9
|
<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元素描述了一个用于layout文件中Binding表达式的属性。
1
2
3
4
5
6
|
<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类型在编译时检查,因此如果一个Variable实现了Observable或observable collection,这应该反映在类型中。(译注:需要查找资料来理解)如果variable是一个没有实现Observable接口的基本类或者接口,Variables不会被observed!
当对于多种配置有不同的layout文件时(如,横向或纵向),Variables会被合并。这些layout文件之间必须不能有冲突的Variable定义。
产生的Binding类对于每一个描述的Variables都会有setter和getter。这些Variables会使用默认的Java值 - null(引用类型)、0(int)、false(boolean)等等,直到调用setter时。
默认情况下,Binding类的命名是基于所述layout文件的名称,用大写开头,除去下划线()以及()后的第一个字母大写,然后添加“Binding”后缀。这个类将被放置在一个模块封装包里的databinding封装包下。例如,所述layout文件contact_item.xml将生成ContactItemBinding。如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding。
Binding类可通过调整data元素中的class属性来重命名或放置在不同的包中。例如:
1
2
3
|
<data
class
=
"ContactItem"
>
...
</data>
|
在模块封装包的databinding包中会生成名为ContactItem的Binding类。如果要想让该类生成在不同的包种,你需要添加前缀.,如下:
1
2
3
|
<data
class
=
".ContactItem"
>
...
</data>
|
在这个情况下,ContactItem类直接在模块包种生成。或者你可以提供整个包名:
1
2
3
|
<data
class
=
"com.example.ContactItem"
>
...
</data>
|
通过使用application namespace以及在属性中的Variable名字从容器layout中传递Variables到一个被包含的layout:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?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>
|
注意:在name.xml以及contact.xml两个layout文件中必需要有uservariable
常用表达式跟Java表达式很像,以下这些是一样的:
三元运算?:
示例:
1
2
3
|
android:text=
"@{String.valueOf(index + 1)}"
android:visibility=
"@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName=
'@{"image_" + id}'
|
缺少的操作:
Null合并操作
1
|
android:text=
"@{user.displayName ?? user.lastName}"
|
函数上的写法如下:
1
|
android:text=
"@{user.displayName != null ? user.displayName : user.lastName}"
|
属性引用
第一个已经在前边提到了a)Data Binding表达式:JavaBean引用的简短格式。
当一个表达式引用一个类的属性,它仍使用同样的格式对于字段、getters以及ObservableFields。
1
|
android:text=
"@{user.lastName}"
|
避免 NullPointerException
Data Binding代码生成时自动检查是否为nulls来避免出现null pointer exceptions错误。例如,在表达式@{user.name}中,如果user是null,user.name会赋予它的默认值(null)。如果你引用user.age,age是int类型,那么它的默认值是0。
集合
常用的集合:arrays、lists、sparse lists以及maps,为了简便都可以使用[]来访问。
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]}"
|
字符串
当使用单引号包含属性值时,在表达式中使用双引号很容易:
1
|
android:text=
'@{map["firstName"]}'
|
使用双引号来包含属性值也是可以的。字符串前后需要使用"`":
1
2
|
android:text=
"@{map[`firstName`]}"
android:text=
"@{map["
firstName
"]}"
|
Resources
使用正常的表达式来访问resources也是可行的:
1
|
android:padding=
"@{large? @dimen/largePadding : @dimen/smallPadding}"
|
格式化字符串和复数可以通过提供参数来判断
1
2
|
android:text=
"@{@string/nameFormat(firstName, lastName)}"
android:text=
"@{@plurals/banana(bananaCount)}"
|
当复数需要多个参数时,所有的参数都会通过:
1
2
3
|
Have an orange
Have %d oranges
android:text=
"@{@plurals/orange(orangeCount, orangeCount)}"
|
一些资源需要显式类型判断:
类型 | 正常引用 | 表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
任何Plain old Java object(POJO)可用于Data Binding,但修改POJO不会导致UI更新。Data Binding的真正能力是当数据变化时,可以通知给你的Data对象。有三种不同的数据变化通知机制:Observable对象、ObservableFields以及observable collections。
当这些可观察Data对象绑定到UI,Data对象属性的更改后,UI也将自动更新。
实现android.databinding.Observable接口的类可以允许附加一个监听器到Bound对象以便监听对象上的所有属性的变化。
Observable接口有一个机制来添加和删除监听器,但通知与否由开发人员管理。为了使开发更容易,一个BaseObservable的基类为实现监听器注册机制而创建。Data实现类依然负责通知当属性改变时。这是通过指定一个Bindable注解给getter以及setter内通知来完成的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
private
static
class
User
extends
BaseObservable {
private
String firstName;
private
String lastName;
@Bindable
public
String getFirstName() {
return
this
.firstName;
}
@Bindable
public
String getFirstName() {
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类文件中生成一个Entry。BR类文件会在模块包内生成。如果用于Data类的基类不能改变,Observable接口通过方便的PropertyChangeRegistry来实现用于储存和有效地通知监听器。
一些小工作会涉及到创建Observable类,因此那些想要节省时间或者几乎没有几个属性的开发者可以使用ObservableFields。ObservableFields是自包含具有单个字段的observable对象。它有所有基本类型和一个是引用类型。要使用它需要在data对象中创建public final字段:
1
2
3
4
5
6
7
|
private
static
class
User
extends
BaseObservable {
public
final
ObservableField<String> firstName =
new
ObservableField<>();
public
final
ObservableField<String> lastName =
new
ObservableField<>();
public
final
ObservableInt age =
new
ObservableInt();
}
|
就是这样,要访问该值,使用set和get方法:
1
2
|
user.firstName.set(
"Google"
);
int
age = user.age.get();
|
一些app使用更多的动态结构来保存数据。Observable集合允许键控访问这些data对象。ObservableArrayMap用于键是引用类型,如String。
1
2
3
4
|
ObservableArrayMap<String, Object> user =
new
ObservableArrayMap<>();
user.put(
"firstName"
,
"Google"
);
user.put(
"lastName"
,
"Inc."
);
user.put(
"age"
,
17
);
|
在layout文件中,通过String键可以访问map:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<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用于键是整数:
1
2
3
4
|
ObservableArrayList<Object> user =
new
ObservableArrayList<>();
user.add(
"Google"
);
user.add(
"Inc."
);
user.add(
17
);
|
在layout文件中,通过索引可以访问list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<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"
/>
|
Binding类的生成链接了layout中variables与Views。如前面所讨论的,Binding的名称和包名可以定制。所生成的Binding类都扩展了android.databinding.ViewDataBinding。
Binding应在inflation之后就立马创建,以确保View层次结构不在之前打扰layout中的binding到views上的表达式。有几个方法可以绑定到一个layout。最常见的是在Binding类上使用静态方法.inflate方法载入View的层次结构并且绑定到它只需这一步。还有一个更简单的版本,只需要LayoutInflater还有一个是采用ViewGroup:
1
2
|
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup,
false
);
|
如果使用不同的机制载入layout,他可一分开绑定:
1
|
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
|
有时Binding不能提前知道,对于这种情况,可以使用DataBindingUtil类来创建Binding:
1
2
3
|
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
|
在layout中对于每个带ID的View会生成一个public final字段。Binding在View层次结构上做单一的传递,提取带ID的Views。这种机制比起某些Views使用findViewById还要快。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<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=
|