原文出自:http://blog.csdn.net/xx326664162/article/details/62048543 薛瑄的博客
虽然官方给出了教程,Data Binding Library(Android Data Binding(数据绑定)用户指南),但是由于近来的更新,发现官方文档并没有更新。有时候看了官方文档,感觉还是不太清楚在讲什么,文章中有些地方我会从实战角度介绍一下,也拓展一些内容,比如双向绑定等。
这篇文章介绍了如何使用Data Binding库,并且用最少的代码来绑定工程中的java文件和layouts文件。
Data Binding库不仅灵活而且广泛兼容- 它是一个support库,因此你可以在所有的Android平台最低能到Android 2.1(API等级7+)上使用它。
需求:Android Plugin for Gradle 1.5.0-alpha1 或 更高版本。
在2015年的谷歌IO大会上,Android UI Toolkit团队发布了DataBinding 框架,将数据绑定引入了Android开发,当时还只支持单向绑定,而且需要作为第三方依赖引入,时隔一年,双向绑定这个特性也得到了支持,同时纳入了Android Gradle Plugin(1.5.0+)中,只需要在gradle配置文件里添加短短的三行,就能用上数据绑定。
android {
....
dataBinding {
enabled = true
}
}
请确保您使用的是Android Studio的兼容版本。Android Studio的Data Binding插件需要Android Studio 1.3.0 或 更高版本。
在继续学习下面内容前,先说一些概念,有个大概印象。Data Binding的使用,需要xml和java的共同配合。在xml中需要data元素,在java中需要Binding类。
Data Binding layout文件有点不同的是:起始根标签是layout,接下来一个data元素以及一个布局。这个布局就是你之前没有使用Data Binding的布局。举例说明如下:
在data内描述了一个名为user的变量属性,使其可以在这个layout中使用:
在layout的属性表达式写作@{},下面是一个TextView的text设置为user的firstName属性:
假设你有一个user的plain-old Java Object(POJO):
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对象:
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文件的名称而产生,将其转换为单词首字母大写并添加“Binding”后缀。上述的layout文件是main_activity.xml,生成的类名是MainActivityBinding。
此类包含layout属性和layout的Views中所有的bindings(例如user变量),并且它还知道如何给Binding表达式赋值。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
就是这样,运行app后,你将会看到Test User。
或者你可以通过如下获取binding:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你在ListView或者RecyclerView adapter使用Data Binding时,你可能会使用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup,
false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
数据绑定允许你编写表达式来处理view分派的事件。事件属性名字取决于监听器方法名字。例如View.OnLongClickListener有onLongClick()的方法,因此这个事件的属性是android:onLongClick。处理事件有两种方法:
零个或多个import元素可以在data元素中使用:
现在,View可以使用Binding表达式:
当类名有冲突时,把一个类重命名为alias::
这样,在该layout文件中Vista对应com.example.real.estate.View,而View对应android.view.View。导入的类型可以以Variable和表达式的形式当做type的参数使用:
注意:Android Studio还没有处理imports,所以自动导入Variable在你的IDE不能使用。您的app仍会正常编译,你可以在您的Variable定义中使用完全符合规定的名称来解决该IDE问题。
还可以在表达式中使用static属性和方法:
…
就像在Java中,java.lang.*是自动导入的。
在data中可以使用任意数量的variable元素。每一个variable元素都可以在layout中使用
在编译时检查Variable类型,因此如果一个Variable实现了Observable或observable collection,这应该反映在类型中。如果variable是一个没有实现Observable接口的基本类或者接口,Variables不会被观察(译者注:不会被观察,就是当变量的值改变,UI不会更新,后续会讲到这个Observable)
当对于多种配置有不同的layout文件时(如,横向或纵向),Variables会被合并。这些layout文件之间不能有冲突的Variable定义。
产生的Binding类对于每一个Variables都有setter和getter方法。这些Variables会使用默认的Java值 - null(引用类型)、0(int)、false(boolean)等等,直到调用setter时。
默认情况下,Binding类的命名是基于所述layout文件的名称,大写单词首字母,除去下划线以及,然后添加“Binding”后缀。这个类将被放置在一个module包里的databinding包下。
例如,所述layout文件contact_item.xml将生成ContactItemBinding。如果module包是com.example.my.app,那么它将被放置在com.example.my.app.databinding。
Binding类可通过调整data元素中的class属性来重命名或放置在不同的包中。例如:
...
在module包的databinding包中会生成名为ContactItem的Binding类。如果要想让该类生成在不同的包中,你需要添加前缀“.”,如下:
...
在这个情况下,ContactItem类直接在module包种生成。或者你可以提供整个包名:
...
通过使用application namespace以及在属性中的Variable名字,从layout中传递Variables到一个 include layout:
注意:在name.xml以及contact.xml两个layout文件中必需要有user variable
Data binding does not support include as a direct child of a merge element. For example, the following layout is not supported:
常用表达式跟Java表达式很像,以下这些是一样的:
数学 + - / * %
字符串连接 +
逻辑 && ||
二进制 & | ^
一元运算 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
instanceof
分组 ()
null
Cast
方法调用
数据访问 []
三元运算 ?:
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
有些符号需要使用他的转移字符
'&' --> '&'
'<' --> '<'
'>' --> '>'
缺少的操作:
this
super
new
显式泛型调用
Null合并操作
android:text="@{user.displayName ?? user.lastName}"
这个操作符等价于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
第一个已经在前边提到了a)Data Binding表达式:JavaBean引用的简短格式。
当一个表达式引用一个类的属性,它仍使用同样的格式对于字段、getters以及ObservableFields。
android:text="@{user.lastName}"
Data Binding代码生成时自动检查是否为nulls来避免出现null pointer exceptions错误。例如,在表达式@{user.name}中,如果user是null,user.name会赋予它的默认值(null)。如果你引用user.age,age是int类型,那么它的默认值是0。
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
android:text='@{map["firstName"]}'
使用双引号来包含属性值也是可以的。字符串前后需要使用"`":
android:text="@{map[`firstName`]}"
android:text="@{map["firstName"]}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化字符串和复数可以通过提供参数来判断
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当复数需要多个参数时,所有的参数都会通过:
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 |
Data Binding的真正作用是当数据变化时,可以通知给Data对象(layout中的data)。从而实现数据改变了,UI也会自动改变。
有三种不同的数据变化通知机制:
第三方库,用于绑定AdapterView
实现android.databinding.Observable接口的类,可以附加一个监听器到Bound对象以便监听对象上的所有属性的变化。
要实现 Observable Binding,首先得有一个 implement 了 android.databinding.Observable接口的类,为了方便,Android 提供了已经封装好的一个类 - BaseObservable,并且实现了监听器的注册机制。
在需要通知的属性的get方法上加上@Bindable,编译阶段会生成BR.[property name],只要调用notifyPropertyChanged()就可以刷新界面了。
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);
}
}
如果只有少许变量,可以使用ObservableField,或者 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.
ObservableFields是自包含具有单个字段的observable对象。要使用它需要在data对象中创建public final字段:
private static class User {
public final ObservableField firstName =
new ObservableField<>();
public final ObservableField lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
调用set方法时,Data Binding Library就会自动的帮我们通知界面刷新了。
user.firstName.set("Google");
int age = user.age.get();
一些app使用更多的动态结构来保存数据。在xml文件中,可以通过键值访问Observable集合。
当集合的key是引用类型,如String,可以使用ObservableArrayMap。
ObservableArrayMap user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在layout文件中,通过String键可以访问map:
…
ObservableArrayList用于键是整数:
ObservableArrayList
在layout文件中,通过索引可以访问list:
…
在一个实际的项目中,相信AdapterView是使用得很多的,使用官方提供给的API来进行AdapterView的绑定需要写很多代码,使用起来不方便,但是由于Data Binding Library提供丰富的扩展功能,所以出现了很多第三方的库来扩展它,下面就来介绍一个比较好用的库binding-collection-adapter,Github地址:https://github.com/evant/binding-collection-adapter
使用的时候在你的build.gradle文件里面添加
compile 'me.tatarka:bindingcollectionadapter:0.16'
如果你要是用RecyclerView,还需要添加
compile 'me.tatarka:bindingcollectionadapter-recyclerview:0.16'
下面就是ViewModel的写法:
您需要提供items和一个ItemBinding绑定到布局。 你应该使用ObservableList,如果有数据变化它会自动更新UI。 但是,如果你不需要该功能,可以使用其他的List。
public class ViewModel {
public final ObservableList items = new ObservableArrayList<>();
public final ItemBinding itemBinding = ItemBinding.of(BR.item, R.layout.item);
}
然后使用app:items和app:itemBinding将其绑定到View。 还有一些便利方法可以通过app:layoutManager将LayoutManager附加到RecyclerView。
然后是item layout:在item布局中,数据的集合将绑定到variable的name,就是传递到ItemBinding中的变量。
如果有多种样式的布局,那么就需要把ItemBinding换成OnItemBind,如下:
public final OnItemBind onItemBind = new OnItemBind() {
@Override
public void onItemBind(ItemBinding itemBinding, int position, String item) {
itemBinding.set(BR.item, position == 0 ? R.layout.item_header : R.layout.item);
}
};
更多用法,Github地址:https://github.com/evant/binding-collection-adapter
双向绑定就是在UI 发生变化,同步更新data中的变量
过去,我们需要自己定义Listener来做双向绑定:
public void change(Editable s) {
final String text = s.toString();
if (!text.equals(name.get()) {
name.set(text);
}
}
现在可以直接使用**@=** 来进行双向绑定了,使用起来十分简单
这样,我们对这个EditText的输入,就会自动set到对应model的name字段上。
##自定义双向绑定
待续…
Binding 类是自动生成的,生成的Binding类链接了layout中variables与Views。如前面所讨论的,Binding的名称和包名可以定制。Binding类都扩展了android.databinding.ViewDataBinding。
Binding类应在inflation之后就立马创建,以确保View层次结构不在之前打扰layout中的binding到views上的表达式。有几个方法可以绑定一个layout。最常见的是在Binding类上使用静态方法.inflate()载入View层次结构。
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);
如果使用不同的机制载入layout,可以分开绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
如果Binding的具体类型不知道,可以使用DataBindingUtil类来创建Binding:
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还要快。例如:
它会生成如下的Binding类:
public final TextView firstName;
public final TextView lastName;
在databinding中,ID几乎没有必要,除非有一些实例需要从代码中访问Views。
每个Variable都有访问方法。
它会在Binding中生成setters和getters:
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跟正常的Views略有不同。他们开始时是不可见的,当他们要么设置为可见或被明确告知要载入时,它们通过载入另外一个layout取代了自己。
由于ViewStub基本上从View的层次结构上消失,在Binding对象的View也必须消失来允许被收集。因为Views是最后的,一个ViewStubProxy对象取带ViewStub,给开发者获得了ViewStub,当它存在以及还可以访问载入的View层次结构时当ViewStub已被载入时。
当载入另一个layout,为新的布局必需创建一个Binding。因此,ViewStubProxy必需监听ViewStub的OnInflateListener监听器并在那个时候建立Binding。因为只有一个可以存在,ViewStubProxy允许开发者在其上设置一个OnInflateListener它会在建立Binding后调用。
有时,不知道具体的Binding类,例如,一个RecyclerView适配器 对任意布局操作,不会知道具体的binding类。它仍然必需在onBindViewHolder期间赋值给Binding。
在这个例子中,该RecyclerView绑定的所有layouts有一个“item”的Variable。该BindingHolder有一个getBinding方法返回ViewDataBinding。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
当一个variable或observable变化时,在下一帧之前binding将会改变。有很多次,但是在Binding时必须立即执行。要强制执行,使用executePendingBindings()方法。
只要它不是一个集合,你可以在后台线程中改变你的数据模型。在判断是否要避免任何并发问题时,Data Binding会对每个Varialbe/field本地化。
每当绑定值更改时,生成的绑定类必须在具有绑定表达式的视图上调用setter方法。数据绑定框架有一些方法来定制调用哪个方法来设置值。
就像Data Binding会自动去查找get方法一下,在遇到属性绑定的时候,它也会去自动寻找对应的set方法。
例如,有关TextView的android:text属性的表达式会寻找一个setText(String)的方法。如果表达式返回一个int,Data Binding会搜索的setText(int)方法。注意:要表达式返回正确的类型,如果需要的话使用casting。
例如,DrawerLayout没有任何属性,但有大量的setters。您可以使用自动setters来使用其中的一个。下面的例子中:由于存在setDrawerListener()方法,所以可以使用app:drawerListener这样的属性
有的setters方法和属性名称并不匹配。对于这些方法,通过BindingMethods注解来与xml中的属性关联。
例如,android:tint属性与setImageTintList相关联,而不与setTint相关。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
以上例子,开发者需要重命名setters是不太可能了,android架构属性已经实现了。
有些xml属性需要自定义它对应的方法。
例如,对于android:paddingLeft属性并没有相关setter。但是setPadding(left, top, right, bottom)是存在在。一个带有BindingAdapter注解的静态绑定适配器方法,可以为一个xml属性自定义setter方法。
Android的属性已经创造了Binding Adapters。举例来说,对于paddingLeft:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding适配器对其他定制类型非常有用。例如,自定义loader可以用来异步载入图像。
当有冲突时,自定义的Binding适配器将覆盖Data Binding默认适配器。
也可以创建接收多个参数的适配器。
@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);
}
如果对于一个ImageViewimageUrl和error都被使用并且imageUrl是一个string类型以及error是一个drawable时,该适配器会被调用。
绑定适配器方法可以在其处理程序中采用旧值。 采用旧值和新值的方法,应该首先具有属性的所有旧值,然后是新值:
@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());
}
}
事件处理程序只能与一个抽象方法接口或抽象类一起使用。 例如:
@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);
}
}
}
当从Binding表达式返回一个对象,一个setter会从自动的、重命名以及自定义的setters中选择。该对象将被转换为所选择的setter的参数类型。
这是为了方便那些使用ObservableMaps来保存数据。例如:
在userMap返回一个对象并且该对象将自动转换为setText(CharSequence)的参数类型。当有关参数类型可能混乱时,开发人员需要在表达式中转换。
有时候转换应该是自动的在特定类型之间。例如,设置背景的时候:
这里,背景需要Drawable对象,但颜色是一个整数。不管何时有Drawable并且返回值是一个整数,那么整数类型会被转换为ColorDrawable。这个转换是通过使用带有BindingConversion注解的静态方法完成的:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
注意:转换仅仅发生在setter级别,因此它是不允许以下混合类型:
可以简化为:
这样CheckBox的状态变更后ImageView会自动改变visibility。
参考:
Android Data Binding(数据绑定)用户指南
QQ 音乐 Android 团队分享 Android DataBinding 数据绑定
Android,DataBinding的官方双向绑定
Android DataBinding 双向绑定
从零开始的Android新项目8 - Data Binding高级篇
棉花糖给 Android 带来的 Data Bindings(数据绑定库)