本篇文章翻译自官方介绍文档(原文)
本篇文章介绍了如何在Android开发中使用Android的Data Binding库来减少开发中的胶水代码(组织布局和业务逻辑的代码)。
Data Binding库支持在Android 2.1(API level 7)及以上版本中使用。
使用Data Binding,Gradle的Android插件需要使用1.5.0-alpha1及以上版本。具体配置可参考如何更新Gradle Android插件?
Data Binding运行环境
要使用Data Binding,首先需要在Android SDK中下载Data Binding的支持包。
然后在应用的构建文件中配置data binding选项,项目的build.gradle文件中配置如下:
1 | android { |
如果你的应用模块依赖的Library模块需要使用data binding,那么在应用模块的build.gradle文件中也要配置data binding。
最后,你需要使用Android Studio 1.3及以上版本,因为这些版本对data binding才有较好的支持,详情可查看Android Studio对Data Binding的支持
Data Binding布局文件修改
你的第一个Data Binding表达式
Data-binding的布局文件与常规的布局文件有一点不同,其根标签是layout标签,然后紧跟着data标签,最后才是常规的布局标签。样例可参考如下:
1 |
|
在data标签里的variable属性user,会在ui布局中使用到
1 | <variable name="user" type="com.example.User"/> |
在ui的描述标签中通过使用**”@{}”**的语法来使用data中申明的变量的属性。如在TextView中将user的firstName设置给TextView的text属性
1 | <TextView android:layout_width="wrap_content" |
Data Object
假设你现在有一个简单的User Java对象(POJO)
1 | public class User { |
这种对象的数据通常一直都不会变,在应用开发中有很多场景都会使用到这种不可变对象。你也可以使用JavaBean对象:
1 | public class User { |
对于data binding来说,以上两种方式是一样的。android:text会获取在表达式**@{user.firstName}中定义的firstName属性,在上面描述的第一种方式对应的就是user.firstName变量,而在第二种方式则对应的是getFirstName()方法,如果方法中有firstName()**方法存在,data binding也会将其对应为firstName属性
如果getFirstName()和firstName()同时存在,会先取getFirstName()方法的内容,getFirstName()方法没有才会去firstName()方法。
绑定数据
绑定类默认会根据布局文件的名称按照“layout文件名的Pascal名称+Binding”的格式生成,如:布局文件名称为main_activity.xml,则生成的的类名称为MainActivityBinding.这个类包含了layout中定义的所要绑定的内容(如:User变量),Binding类知道如何将变量的值对应到layout中引用的表达式中。绑定数据最简单的方法就是在infalte的过程中操作:
1 |
|
以上绑定类生成和设置变量的所有流程。再次获取binding对象可通过如下方式
1 | MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater()); |
如果你在ListView或者RecyclerView的adapter中绑定列表的Item,可以这样使用
1 | ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false); |
事件处理
Data Binding允许表达式处理view各种分发的事件(如:onClick监听)。除了一小部分特殊情况外,事件的属性通常是view的listener的方法名称,如view.OnLongClickListener对应的方法为onLongClick()。在XML的表达式的事件处理则为android:onLongClick。在Data Binding中有如下两种方式来做事件处理。
方法引用(Method Reference):在你的事件表达式中,你可以引用符合监听器方法签名的处理方法。 当表达式求值为方法引用时,Data Binding会在监听器中封装方法的引用和其所有者对象,并在目标View上设置该监听器。 如果表达式的值为null,则数据绑定不会创建监听器,而是设置一个空监听器。
监听器绑定(Listener Bindings):此种方式的事件表达式是一个lambda表达式。通常lambda表达式会在在view的事件发生时再生效。Data Binding默认总是创建一个监听器,并设置在对应View上。当view分派事件时,监听器再计算lambda表达式的值,最终作用在lambda表达式中声明的处理方法上。
方法引用(Method Reference)
事件可以直接绑定到处理程序方法,类似于android:onClick的方式可以分配到Activity中的一个方法。 与View#onClick属性相比,一个主要优点是表达式在编译时处理,因此如果方法不存在或其签名不正确,你会收到编译时错误。
方法引用和监听器绑定之间的主要区别是,方法引用中的实际监听器实现是在数据绑定时创建的,而不是在触发事件时创建的。如果你喜欢在事件发生时再计算表达式,则应使用监听器绑定方式。
要将事件分配给其处理程序,需要使用正常绑定表达式,其值为要调用的方法名称。 例如,如果你的事件处理对象如下所示:
1 | public class MyHandlers { |
绑定表达式可以为view分配点击监听器:
1 |
|
请注意,表达式中的方法的签名必须与监听器对象中方法的签名完全一致。
监听器绑定(Listener Bindings)
监听器绑定是在事件发生时才运行的绑定表达式。它类似于方法引用,但是允许你运行任意数据绑定表达式(不限制处理方法参数)。此功能适用于Android Gradle插件2.0及更高版本。
在方法引用中,方法的参数必须与事件监听器的参数匹配。在监听器绑定中,只要你的返回值与监听器的预期返回值匹配(除非它期望void)即可。例如,你可以有一个presenter类,它具有以下方法:
1 | public class Presenter { |
然后,你可以将点击事件绑定到你的Presenter类中,如下所示:
1 |
|
监听器由lambda语句表示,并且lambda语句只允许作为表达式的根元素来使用(译者注:这里可以理解为在箭头左右的语句中不能再使用lambda语句)。Data Binding自动为事件创建监听器或注册者,当View触发事件时,Data Binding再计算给定的表达式。在常规绑定表达式中,在计算这些监听器表达式时,Data Binding会保证绑定表达式中引用变量的空值安全和线程安全性。
注意,在上面的示例中,我们没有定义传递到onClick(android.view.View)的View参数。监听器绑定方式为监听器参数提供两个选择:你可以忽略方法的所有参数或命名出所有参数出来。如果你喜欢命名参数,可以在表达式中使用它们。 例如,上面的表达式可以写成:
1 | android:onClick="@{(view) -> presenter.onSaveClick(task)}" |
如果你想使用表达式中的参数,如下所示:
1 | public class Presenter { |
1 | android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}" |
你还可以使用具有多个参数的lambda表达式:
1 | public class Presenter { |
1 | <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content" |
如果正在监听的事件返回类型不是void的值,则表达式也必须返回相同类型的值。 例如,如果你想监听长点击事件,你的表达式应该返回布尔值。
1 | public class Presenter { |
1 | android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}" |
如果由于空对象而无法计算表达式,Data Binding将返回该类型的默认Java值。 例如,引用类型为null,int为0,boolean为false等。
如果需要使用带断言(例如三元表达式)的表达式,则可以使用void作为空操作符号。
1 | android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}" |
避免复杂的监听器
监听器表达式非常强大,可以使你的代码容易阅读理解。另一方面,如果包含复杂表达式的监听器会使你的布局难以阅读和维护。这些表达式应该像将数据从UI层传递到处理方法中一样简单,由监听器表达式调用的处理方法来实现所有的业务逻辑。
有一些特殊的单击事件处理程序可能会与View.OnClickListener的onClick(View)方法冲突,所以他们需要另外一个属性来代替而不是android:onClick方法,从而来避免冲突。下表是为避免此类冲突而创建的特殊属性:
类名 | Listener设置方法 | 属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局详解(Layout Details)
导入(Imports)
在数据元素内可以使用零个或多个导入元素。 导入语句允许像Java一样在布局文件中引用类。
1 | <data> |
现在,在你的绑定表达式中可以使用View:
1 | <TextView |
当有类名冲突时,其中一个类可以重命名为“alias:”
1 | <import type="android.view.View"/> |
现在,Vista可以用于引用com.example.real.estate.View而View可以用于在布局文件中引用android.view.View。 导入类型可以用作变量和表达式中的类型引用:
1 | <data> |
注意:Android Studio尚不能自动处理导入,因此导入的变量的自动完成功能当前无法在IDE中工作。你的应用程序可能仍然会编译良好,但是运行时可能会引起错误,因此你可以通过在变量定义中使用完全限定名来解决IDE的问题。
1 | <TextView |
在引用表达式中的静态字段和方法时,也可以使用导入类型:
1 | <data> |
跟Java中一样,java.lang.*是自动导入的。
变量(Variables)
在数据元素内部可以使用任何数量的变量。变量的每一个属性都可以作为布局文件中绑定表达式使用的属性。
1 | <data> |
Data Binding会在编译时检查变量类型,因此如果变量实现了Data Binding中的Observable接口或者是一个Observable集合接口,那么还需要在对应变量属性上加上标示。如果变量没有实现**Observable***接口的基类或接口,变量不会被观察!
当应用中存在用于各种配置(例如,横向或纵向)的不同布局文件时,变量将被组合。 这些布局文件之间不能有冲突的变量定义。
生成的绑定类具有每个描述的变量的setter和getter方法。 在变量的setter方法调用之前,变量将采用默认的Java值:引用类型为null,int为0,boolean为false等。
Data Binding默认会生成一个名为context的特殊变量,以根据需要用于绑定表达式。context的值是来自根View的getContext()的context。context变量将被具有该名称的显式context声明覆盖。
自定义Binding类名称(Custom Binding Class Names)
默认情况下,Binding类是基于布局文件的名称生成的,以大写字母开头,除去下划线(_),然后加上“Binding”。 这个类将被放置在模块包下的数据绑定包中。 例如,布局文件contact_item.xml将生成ContactItemBinding。 如果模块包是com.example.my.app,那么它将被放置在com.example.my.app.databinding中。
通过调整data元素的class属性,绑定类可以重命名或放置在不同的包中。 例如:
1 | <data class="ContactItem"> |
这将在模块包中的数据绑定包中生成绑定类作为ContactItem。 如果类应该在模块包内的不同包中生成,则可以使用“.”作为前缀。
1 | <data class=".ContactItem"> |
在如下示例中,ContactItem直接在模块包中生成。 如果提供完整包名,则绑定类就会生成在指定包名下:
1 | <data class="com.example.ContactItem"> |
布局引用(Includes)
data中声明的变量可以通过bind属性将其传递到被include的布局中:
1 |
|
上面的代码中,在name.xml和contact.xml布局文件中都必须有一个用户变量。
数据绑定不支持include作为merge元素的直接子代。 例如,不支持以下布局:
1 |
|
表达式语法(Expression Language)
通用语法
通用语法跟在Java中使用的大多类似,如下所示:
- 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 | android:text="@{String.valueOf(index + 1)}" |
不支持的操作符
在Java中支持但是在Data Binding的表达式语法不支持的是:
- this
- super
- new
- Explicit generic invocation(显式泛型调用)
空合并运算符(Null Coalescing Operator)
空合并运算符(??):如果左操作数不为空,则选择左操作数否则选择右操作数。
1 | android:text="@{user.displayName ?? user.lastName}" |
上面等价于
1 | android:text="@{user.displayName != null ? user.displayName : user.lastName}" |
属性引用(Property Reference)
在第一节已经讨论过:JavaBean引用简写。对于类变量:fiels,getters或者ObservabeFiels方式都是一样的
1 | android:text="@{user.lastName}" |
空指针异常处理(Avoiding NullPointerException)
生成的数据绑定代码会自动检查null并避免空指针异常。 例如,在表达式@ {user.name}中,如果user为null,则将为user.name分配其默认值(null)。 如果你引用了user.age,其中age是一个int,那么它将默认为0。
集合(Collections)
为了方便,可以使用[]运算符来访问公共集合:数组,List,sparse list和Map。
1 | <data> |
字符语法(String Literals)
当属性值使用单引号引用时,表达式中需使用双引号:
1 | android:text='@{map["firstName"]}' |
也可以使用双引号引用属性值,字符串文字应该使用 ‘ 或后引号 `
1 | android:text="@{map[`firstName`}" |
资源(Resources)
可以使用正常语法来访问资源:
1 | android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" |
格式字符串和复数可以使用参数:
1 | android:text="@{@string/nameFormat(firstName, lastName)}" |
1 | Have an orange |
一些资源需要在表达式中使用特定类型。
类型 | 正常类型 | 表达式类型 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
数据对象(Data Objects)
所有普通的Java对象(POJO)都可以用于数据绑定,但是修改POJO不会导致UI更新。通过为数据对象提供数据更改时通知的能力,可以使用Data Binding的真正强大功能。 有三种不同的数据更改通知机制,可观察对象(Observable objects),可观察字段(Observable fields)和可观察的集合(Observable collections)。
当这些可观察的数据对象绑定到UI,当数据对象的属性更改时,UI将会自动更新。
可观察对象(Observable Objects)
实现Observable接口的类允许将单个listener附加到绑定对象上,以监听该对象上所有属性的更改。
Observable接口具有添加和删除listener的机制,但通知动作是由开发人员决定的。 为了使开发更容易,Data Binding库创建了一个BaseObservable基类来实现监听器注册机制。 数据类实现者仍然负责通知属性何时更改。 通过分配一个Bindable Annotation到getter方法同时在setter方法中发送通知。
1 | private static class User extends BaseObservable { |
Bindable注解编译期间将在模块包中生成BR类文件,同时在BR类文件中生成一个条目。如果数据类的基类不能被改变,则Observable接口可以使用方便的PropertyChangeRegistry来实现,以有效地存储和通知监听器。
可观察属性(ObservableFields)
有一些工作涉及到创建Observable类,所以开发人员想要节省时间或少量属性可以使用ObservableField及ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble和ObservableParcelable。 ObservableField其实是具有单个字段的自包含ObservableObject。 基本类型的观察对象避免了在访问操作期间的装箱和拆箱操作。 使用时,在数据类中创建对应的public final字段:
1 | private static class User { |
取值时,使用取值方法:
1 | user.firstName.set("Google"); |
可观察集合(Observable Collections)
一些应用程序使用更多动态数据结构来保存数据。可观察集合包装了对这些数据对象的操作。 当键是引用类型(例如String)时,建议使用ObservableArrayMap。
1 | ObservableArrayMap<String, Object> user = new ObservableArrayMap<>(); |
在布局文件中,可以通过String的key访问map:
1 | <data> |
ObservableArrayList类似于Java中的ArrayList:
1 | ObservableArrayList<Object> user = new ObservableArrayList<>(); |
在layout文件中,可以通过索引访问列表:
1 | <data> |
生成绑定类(Generate Binding)
生成的绑定类将布局变量与布局中的视图相关联。 如前所述,绑定的名称和包可以自定义。 生成的绑定类都继承自ViewDataBinding。
创建(Creating)
绑定类应该在view inflate后立即创建,从而确保在布局中的表达式绑定到视图之前,View层次结构不会受到干扰。 有几种方法绑定到布局。 最常见的是在Binding类上使用静态方法。inflate方法一次性填充Layout并绑定到Layout中View的所有层次结构。 有一个更简单的版本,只需要一个LayoutInflater和一个还需要一个ViewGroup:
1 | MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater); |
如果布局是使用不同的机制inflate,则可以单独绑定:
1 | MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot); |
有时,绑定不能提前知道。 在这种情况下,可以使用DataBindingUtil类创建绑定:
1 | ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, |
带Id的View(Views with IDs)
将在布局中为每个具有Id的每个视图生成公共final字段。 绑定在View层次结构上执行单次传递,使用Id来提取视图。 这个机制可能比为几个视图调用findViewById更快。 例如:
1 | <layout xmlns:android="http://schemas.android.com/apk/res/android"> |
以上带Id的view会在binding类中生成如下字段
1 | public final TextView firstName; |
Data Binding中几乎没有使用Id的必要,但仍然有一些情况下可能需要从代码访问视图。
变量(Variables)
每一个变量都会被赋予访问方法
1 | <data> |
在binding类中会生成set和get方法
1 | public abstract com.example.User getUser(); |
ViewStub
ViewStub与普通View有点不同。 它们从不可见状态开始,当它们被显示或被明确告诉inflate时,他们在布局中通过inflate另一个布局来替换自己。
因为ViewStub基本上从View层次结构中消失,所以绑定对象中的View也必须消失以允许回收。 因为View是final的,所以采用ViewStubProxy对象代替ViewStub,当ViewStubProxy存在时,它允许开发人员通过它来访问ViewStub,并且当ViewStub被inflate后,还可以访问其填充后的View层次结构。
当inflate另一个布局时,必须为新布局建立绑定。 因此,ViewStubProxy必须监听ViewStub的ViewStub.OnInflateListener并在那时建立绑定。 由于只有一个可以存在,ViewStubProxy允许开发人员设置一个OnInflateListener,它将在建立绑定后调用。
高级绑定(Advanced Binding)
动态变量(Dynamic Variables)
有时,绑定类不一定知道具体的变量类型。 例如,对任意布局操作的RecyclerView.Adapter不会知道绑定类的数据类型。 它仍然必须在onBindViewHolder(VH,int)期间分配绑定值。
在如下示例中,RecyclerView绑定的所有布局都有一个“item”变量。 ViewHolder的getBinding方法返回ViewDataBinding基类。
1 | public void onBindViewHolder(BindingHolder holder, int position) { |
快速绑定(Immediate Binding)
当变量或可观察数据变化时,系统调度绑定类将在下一帧之前改变。 但是,如果必须立即执行绑定,可以强制执行,使用executePendingBindings()方法。
后台线程(Background Thread)
你可以在后台线程中更改非集合的数据模型。Data Binding会在计算时将每个变量/字段在各个线程做一份数据拷贝,以避免任何并发问题。
xml属性设置器(Attribute Setters)
每当Data的变量数据更改时,绑定类必须将更改的值设置的对应的View的属性中去。Data Binding框架提供了多种方式来实现View属性的设置
自动设置器(Automatic Setters)
对于View中的属性,Data Binding会直接查找对应的set方法。属性的命名空间无关紧要,只有属性名称本身。
例如:android:text=user.age,与TextView的属性相关联的表达式将查找setText(String)。 如果user.age是一个int型数据,Data Biding将搜索一个setText(int)方法。所以表达式需要返回正确的类型,如果必要,需要进行格式的转换,(这里需要注意的是,即使View中没有给定名称的属性,Data Binding依然会工作)。你可以通过使用Data Binding轻松地为任何View中的任何set方法“创建”属性。 例如,support包下的DrawerLayout没有任何xml属性,但有大量的set方法。 你可以使用自动设置器来使用其中的任何一个。
1 | <android.support.v4.widget.DrawerLayout |
重命名设置器(Renamed Setters)
某些View的set方法跟xml中的属性不一致。例如ImageView有一个setImageTintListener(ColorStateList tint)的方法,但是xml文件中对应的描述属性是android:tint,对于这些方法,属性可以通过BindingMethods注解与设置器相关联。 这必须与一个类相关联,并包含BindingMethod注释,每个重命名方法一个。 例如,android:tint属性与setImageTintList(ColorStateList)相关联,而不是setTint。
1 |
|
对于Framework层的空间属性,大部分已经实现了DataBinding属性名,不需要开发者自己重命名Framework层的控件
自定义设置器(Custom Setters)
一些属性需要自定义绑定逻辑。 例如,没有为android:paddingLeft属性的关联set方法,而只存在setPadding(left,top,right,bottom)方法。 使用BindingAdapter注解的静态绑定适配器方法允许开发人员自定义属性调用的set方法。
android属性已经创建了paddingLef的BindingAdapter。如下所示:
1 |
|
BindingAdapter也可用于自定义其他类型。 例如,可以调用自定义加载器以加载离线图片。
当自定义属性和系统属性发生冲突时,开发人员创建的自定义BindingAdapter将覆盖Data Binding的默认适配器。
绑定适配器还可接收多参数
1 |
|
1 | <ImageView app:imageUrl="@{venue.imageUrl}" |
如果imageUrl和error都用于ImageView,而imageUrl是字符串并且error是drawable,则调用此适配器。
- 在匹配期间将忽略自定义命名空间。
- 你也可以为android命名空间编写适配器。
绑定适配器方法可以选择地在其处理程序中采用旧值。 采用旧值和新值的方法应该具有属性的所有旧值,然后是新值:
1 |
|
事件处理器只能使用抽象类或接口的一个抽象方法,如下所示:
1 |
|
当侦听器有多个方法时,它必须拆分成多个侦听器。 例如,View.OnAttachStateChangeListener有两个方法:onViewAttachedToWindow()和onViewDetachedFromWindow()。 然后,我们必须创建两个接口来区分它们的属性和处理程序。
1 |
|
因为改变一个监听器也会影响另一个监听器,所以我们必须有三个不同的绑定适配器,一个用于每个属性,一个用于两个属性,它们都应该被设置。
1 |
|
上面的例子比正常情况稍微复杂一些,因为View使用add和remove作为监听器,而不是View.OnAttachStateChangeListener的set方法。 android.databinding.adapters.ListenerUtil类可以帮助跟踪以前的监听器,以便它们可以在绑定Adapter中删除。
通过用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注解接口OnViewDetachedFromWindow和OnViewAttachedToWindow,数据绑定代码生成器知道监听器应该只在Honeycomb MR1和新设备上运行时生成。
同理addOnAttachStateChangeListener(View.OnAttachStateChangeListener)与上述一样。
转换器(Converters)
对象转换(Object Conversions)
当从绑定表达式返回对象时,将从自动,重命名和自定义set方法三种方式中选择一个set方法。 对象将被转换为所选择的set方法的参数类型。
比如使用ObservableMaps来保存数据:
1 | <TextView |
userMap根据key值获取到一个对象,该对象将被自动转换为在setText(CharSequence)方法中的参数类型。 当可能存在关于参数类型歧义时,开发人员需要在表达式中显示转换。
自定义类型转换(Custom Conversions)
有时,类型应在特定类型之间自动进行。 例如,当设置背景时:
1 | <View |
上面代码中,View的背景应该采用Drawable,但是颜色是数值代表的。 每当一个Drawable被期望并返回一个整数,int应该被转换为ColorDrawable。 此转换是使用带有BindingConversion注解的静态方法完成的:
1 |
|
请注意,转换只发生在设置方法级别,因此不允许使用像这样的混合类型:
1 | <View |
Android Studio对Data Binding的支持
Android Studio支持许多用于Data Binding代码的编辑功能。 例如,它支持Data Binding表达式的以下功能:
- 语法高亮
- 标记表达式语法错误
- XML代码自动提示
- 资源引用,包括导航(如导航到声明)和快速文档
注意:数组和泛型类型(如Observable类)可能在没有错误时显示错误。
布局“preview”窗口显示Data Binding的默认值(如果有的话)。 在以下示例中,从布局XML文件中截取元素,“preview”窗口在TextView中显示PLACEHOLDER默认文本值。
1 | <TextView android:layout_width="wrap_content" |
如果需要在项目的设计阶段显示默认值,还可以使用**tools:**属性而不是默认表达式值