关于 Data Binding
Data Binding 解决了 Android UI 编程的一个痛点,官方原生支持 MVVM 模型可以让我们在不改变既有代码框架的前提下,非常容易地使用这些新特性。
Data Binding 框架如果能够推广开来,也许 RoboGuice、ButterKnife 这样的依赖注入框架会慢慢失去市场,因为在 Java 代码中直接使用 View 变量的情况会越来越少。
android {
dataBinding {
enabled true
}
}
- DataBinding 出现以前,我们在实现 UI 界面时,不可避免的编写大量的毫无营养的代码:比如 View.findViewById(),比如各种更新 View 属性的 setter:setText(),setVisibility(),setEnabled() 或者 setOnClickListener() 等等。
- 这些“垃圾代码”数量越多,越容易滋生 bug。
- 使用 DataBinding,我们可以避免书写这些“垃圾代码”。
- 使用 Data Binding 会增加编译出的 apk 文件的类数量和方法数量。
- 新建一个空的工程,统计打开 build.gradle 中 Data Binding 开关前后的 apk 文件中类数量和方法数量,类增加了 120+,方法数增加了 9k+(开启混淆后该数量减少为 3k+)。
- 如果工程对方法数量很敏感的话,请慎重使用 Data Binding。
基础功能的完整案例
【编写布局文件】
使用 Data Binding 之后,xml 的布局文件就不再用于单纯地展示 UI 元素,还需要定义 UI 元素用到的变量。所以,它的根节点不再是一个 ViewGroup,而是变成了 layout,并且新增了一个节点 data。
… //新增的data节点
... //原先的根节点
要实现 MVVM 的 ViewModel 就需要把数据(Model)与 UI(View) 进行绑定,data 节点的作用就像一个桥梁,搭建了 View 和 Model 之间的通路。
定义一个 POJO 类:
public class User {
private String firstName;//就是一个简单的Java Bean
private String lastName;
...
}
稍后,我们会在 xml 布局文件的 data 节点中声明一个 User 类型的 variable,这个变量会为 UI 元素提供数据,然后在 Java 代码中把『后台』数据与这个 variable 进行绑定。
回到布局文件,在 data 节点中声明一个 User 类型的变量 user。
其中 type 属性就是我们刚刚定义的 User 类。
当然,data 节点也支持 import,并且 import 并没有要求一定要放在使用前,所以上面的代码可以这样写:
注意:
- java.lang.* 包中的类会被自动导入,可以直接使用。
- 基本类型的type可以使用包装类或直接使用原型,比如,整数可以使用【type="int"】或【type="Integer"】
我们 build 工程后会自动在 build 目录下生成一个继承自 ViewDataBinding 的类,这个类将被放置在databinding包下。比如,如果我们的包名是com.bqt.databinding,那么它将被放置在com.bqt.databinding.databinding包下。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//用 DatabindingUtil.setContentView() 来替换掉 setContentView()
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
...
}
【Java代码中绑定variable】
在创建**Binding类的实例后,我们便可以使用它来绑定variable,例如:
User user = new User("Test", "User");
binding.setUser(user);
注意,所有的 set/get 方法都是根据 variable 名称生成的,例如,上面布局中定义了一个 name="user" 的变量:
那么就会生成对应的 set/get 方法:
public void setUser(com.bqt.databinding.model.User User) { ... }
public com.bqt.databinding.model.User getUser() { ... }
【布局中使用variable】
数据与variable 绑定之后,通过 @{} 可以直接把 Java 中定义的属性值赋值给 xml 中 UI 元素的某个属性。
至此,一个简单的数据绑定就完成了。
Observable Binding
任何Plain old Java object(POJO)可用于Data Binding,但修改POJO不会导致UI更新。比如在上面我们的案例中,我们通过 binding.setUser(user) 将数据 User 和 View 绑定在了一起,我们本想着,在 User 改变之后, View 会自动更新,但实际上是没有这种效果的。
Data Binding的真正能力是当数据变化时,可以通知给你的Data对象。有三种不同的数据变化通知机制:Observable对象、ObservableFields以及Observable集合。当这些可观察Data对象绑定到UI,Data对象属性的更改后,UI也将自动更新。
Observable和BaseObservable
实现 android.databinding.Observable 接口的类可以允许添加一个监听器到 Bound 对象以便监听对象上的所有属性的变化。Observable 接口有一个机制可以添加和删除监听器,但通知与否由开发人员管理。
为了使开发更容易,一个 BaseObservable 的基类为实现监听器注册机制而创建。Data实现类依然负责通知当属性改变时。这是通过指定一个@Bindable注解给getter以及setter内通知来完成的。
- ObservableField
- ObservableParcelable
- ObservableParcelable
- ObservableBoolean, ObservableByte, ObservableChar, ObservableDouble, ObservableFloat, ObservableInt, ObservableLong, ObservableShort
- [扩充的方法] synchronized void notifyChange():Notifies listeners that all properties of this instance have changed.
- [扩充的方法] void notifyPropertyChanged(int fieldId):Notifies listeners that a specific property has changed.
- [实现的方法] synchronized void addOnPropertyChangedCallback(Observable.OnPropertyChangedCallback callback):Adds a callback to listen for changes to the Observable.
- [实现的方法] synchronized void removeOnPropertyChangedCallback(Observable.OnPropertyChangedCallback callback):Removes a callback from those listening for changes.
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with @Bindable to generate a field in BR to be used as fieldId.
* @param fieldId The generated BR id for the Bindable field.
*/
public void notifyPropertyChanged(int fieldId) {
synchronized (this) {
if (mCallbacks == null) return;//private transient PropertyChangeRegistry mCallbacks;
}
mCallbacks.notifyCallbacks(this, fieldId, null);//通过【PropertyChangeRegistry】来实现通知监听器
}
public class ObservableUser extends BaseObservable {
private String name;
@Bindable//给getter方法添加注解。The getter for an observable property should be annotated with Bindable.
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);//通知某个属性改变了。Notifies listeners that a specific property has changed.
}
}
BR 是编译阶段生成的一个类,功能与 R.java 类似。
在编译期间,通过@Bindable注解标记的getter方法返回的字段会在BR类文件中生成一个同名的Entry,如下:
package com.bqt.databinding;
public class BR {
...
public static final int name = 17;
...
}
Observable field
除此之外,还有一种更细粒度的绑定方式,可以具体到成员变量,这种方式无需继承 BaseObservable,一个简单的 POJO 就可以实现。
系统为我们提供了所有的原始类型所对应的 Observable 类,例如ObservableInt、ObservableFloat、ObservableBoolean等等,还有一个 ObservableField 对应着 引用类型。这种方式非常适合那些几乎没有几个属性的 POJO 。
public class android.databinding.ObservableInt extends BaseObservable implements Parcelable Serializable
ObservableInt:An observable class that holds a primitive int.
ObservableField:An object wrapper to make it observable.
Observable field classes may be used instead of creating an Observable object.
Fields of this type should be declared final because bindings only detect检测 changes in the field's value, not of the field itself.
This class is parcelable可扩展的 and serializable but callbacks are ignored when the object is parcelled / serialized. Unless you add custom callbacks, this will not be an issue because data binding framework always re-registers callbacks when the view is bound.
要使用它需要在data对象中创建public final字段,如:
public class PlainUser {
public final ObservableField name = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
要访问该值,使用set和get方法:
PlainUser plainUser = new PlainUser();
plainUser.name.set("包青天,ObservableField");
plainUser.age.set(27);
int age = plainUser.age.get();
Observable集合
一些app使用更多的动态结构来保存数据,Observable集合允许键控访问这些data对象。
【ObservableArrayMap】
public class android.databinding.ObservableArrayMap extends ArrayMap implements ObservableMap
ObservableArrayMap用于键是引用类型,如String。
ObservableArrayMap mapUser = new ObservableArrayMap<>();
mapUser.put("name", "ObservableArrayMap");
mapUser.put("age", 27);
在layout文件中,通过String键可以访问map。
android:text='@{mapUser["name"]}'
android:text='@{String.valueOf(1 + (Integer)mapUser["age"])}'
注意,在XML布局的某个属性值中使用泛型时,要用【<】来代替【<】
public class android.databinding.ObservableArrayList extends ArrayList implements ObservableList
ObservableArrayList
在layout文件中,通过索引可以访问list:
android:text='@{listUser[0]}'
android:text='@{String.valueOf(1 + (Integer)listUser[1])}'
双向绑定
在布局中引用variable时,将之前的【@{}】改成了【@={}】即可实现双向绑定,比如通过【@={}】将一个变量和EditText的内容绑定在一起,当用户更改EditText中的内容时,和它绑定的变量也会同步改变。
android:text="@{user.name}"//通过【 @{user.name} 】方式绑定时,当用户更改EditText中的内容时,和它绑定的变量【不会】同步改变
android:text="@={user2.name}"//通过【 @={user.name} 】方式绑定时,当用户更改EditText中的内容时,和它绑定的变量【会】同步改变
常用表达式
常用表达式跟Java表达式很像,以下这些是一样的:
- 数学 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二进制 & | ^
- 一元运算 + - ! ~
- 移位 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 分组 ()
- null
- Cast
- 方法调用
- 数据访问 []
- 三元运算 ? :
缺少的操作:
- this
- super
- new
- 显式泛型调用
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
集合元素的访问
常用的集合:arrays、lists、sparse lists以及maps,为了简便都可以使用 [] 来访问。
android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"
注意,在XML布局的某个属性值中使用泛型时,要用转义字符【<】来代替【<】,否则编译失败:
> org.xml.sax.SAXParseException; 与元素类型 "variable" 相关联的 "type" 属性值不能包含 '<' 字符。
可以使用单引号包含属性值,而在表达式中使用双引号:
android:text='@{map["name"]}'
也可以使用双引号来包含属性值,而在字符串前后使用【`】,这个是ESC下面、Tab上面的那个键:
android:text="@{map[`name`]}"
android:text="@{map["name"]}"
也可以使用双引号来包含属性值,而在字符串前后使用转义字符【"】:
android:text="@{map["name"]}"
PS,XML中需要的转义字符:
&(逻辑与) &
<(小于) <
>(大于) >
"(双引号) "
'(单引号) '
使用资源数据 Resources
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
//dimens.xml中的定义
20dp
android:padding="@{large ? (int)@dimen/largePadding : (int)@dimen/smallPadding}" //错误写法
android:text="@{@string/nameFormat(firstName, lastName)}"
Full Name: %1$s %2$s //格式化字符串
其他一些小知识点
android:text="@{MyStringUtils.upper(user.firstName)}"
android:text="@{User.SEX}"
... //放在package的根目录,即上述databinding的父目录
... //提供整个包名
【Null合并操作】
??运算符:左边的对象如果它不是null,选择左边的对象;或者如果它是null,选择右边的对象:
android:text="@{user.displayName ?? user.lastName}"
//等价于:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
【带 ID 的 View】
只要给 View 定义一个 ID,Data Binding 就会为我们生成一个对应的 final 字段。Binding在View层次结构上做单一的传递,提取带ID的Views。这种机制比起某些Views使用findViewById还要快。例如:
android:id="@+id/firstName"
binding.firstName.setText("包");//firstName是DataBinding自动生成一个对应的变量
属性中的 variable 名字从容器 layout 中传递到被包含的 layout。
注意:在include的layout文件中定义的variable必须在外部也要定义,比如下例中,在 user.xml 中必需要有 user variable。
【避免 NullPointerException】
Data Binding代码生成时自动检查是否为nulls来避免出现NullPointerException错误。
例如,在表达式 @{user.name} 中,如果user是null,则 user.name 会赋予它的默认值(null)。如果你引用 user.age(age是int类型),那么它的默认值是0。
【直接Binding】
当一个variable或observable变化时,binding会在下一帧之前被计划要改变。有很多次,但是在Binding时必须立即执行。要强制执行,使用executePendingBindings()方法。
Evaluates评估 the pending bindings, updating any Views that have expressions bound to modified variables. This must be run on the UI thread.
【后台线程】
只要它不是一个集合,你可以在后台线程中改变你的数据模型。在判断是否要避免任何并发问题时,Data Binding会对每个Varialbe/field本地化。
使用ViewStubs
xml 文件与之前的代码一样,根节点改为 layout,在 LinearLayout 中添加一个 ViewStub,添加 ID。
编译后会在 Binding 中生成同名的 ViewStubProy 类型的成员:
public final android.databinding.ViewStubProxy mViewStub;
在 Java 代码中,通过 Binding 实例为 ViewStubProy 注册 ViewStub.OnInflateListener 事件,当监听到ViewStub的OnInflateListener事件时需要为新的布局创建一个Binding:
mBinding.mViewStub.setOnInflateListener(new ViewStub.OnInflateListener() {//监听ViewStub的OnInflateListener监听器
@Override
public void onInflate(ViewStub stub, View inflated) {//当载入另一个layout时为新的布局创建一个Binding
MViewStubBinding binding = DataBindingUtil.bind(inflated);//同样,此Binding的名字取决于ViewStub布局的名字。
User user = new User("fee", "lang");
binding.setUser(user);//此Binding只能处理ViewStub布局中带id的View或变量,而不能处理根布局中的东西
}
});
//填充ViewStub
if (!mBinding.mViewStub.isInflated()) mBinding.mViewStub.getViewStub().inflate();
动态Variables,如RecyclerView
static class UserAdapter2 extends RecyclerView.Adapter {
private List mUsers;
public UserAdapter2(List mUsers) {
this.mUsers = mUsers;
}
static class UserHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;
public UserHolder(View itemView) {
super(itemView);
}
public ViewDataBinding getBinding() {
return binding;
}
public void setBinding(ViewDataBinding binding) {
this.binding = binding;
}
}
@Override
public int getItemCount() {
return mUsers.size();
}
@Override
public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()),
R.layout.user_item, viewGroup, false);//【在 onCreateViewHolder 的时候创建这个 DataBinding】
UserHolder holder = new UserHolder(binding.getRoot());
holder.setBinding(binding);
return holder;
}
@Override
public void onBindViewHolder(UserHolder holder, int position) {
ViewDataBinding binding = holder.getBinding();//【在 onBindViewHolder 中获取这个 DataBinding】
binding.setVariable(BR.user, mUsers.get(position));
binding.executePendingBindings();
}
}
还有另外一种比较简洁的方式,直接在构造 Holder 时把 View 与自动生成的 XXXBinding 进行绑定。
static class UserAdapter extends RecyclerView.Adapter {
private List mUsers;
public UserAdapter(List mUsers) {
this.mUsers = mUsers;
}
static class UserHolder extends RecyclerView.ViewHolder {//最主要的区别就是在这里!
private UserItemBinding mBinding;
public UserHolder(View itemView) {
super(itemView);
mBinding = DataBindingUtil.bind(itemView);//【在构造 Holder 时把 itemView 与自动生成的 XXXBinding 进行绑定】
}
public void bind(User user) {
mBinding.setUser(user);//绑定数据
}
}
@Override
public int getItemCount() {
return mUsers.size();
}
@Override
public UserHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.user_item, viewGroup, false);
return new UserHolder(itemView);
}
@Override
public void onBindViewHolder(UserHolder holder, int position) {
holder.bind(mUsers.get(position));//只需要为每一个item绑定数据,而不需要手动操作item中的UI
}
}
属性 Setters
自动Setters
同样,对于自定义View,即使属性没有在 declare-styleable 中定义,我们也可以通过 xml 进行赋值操作。
为了演示这个功能,我自定义了一个 View - NameCard,属性资源 R.styleable.NameCard 中只定义了一个 age 属性:
其中 firstName 和 lastName 有对应的两个 setter 方法(前提条件):
public class NameCard extends LinearLayout {
private TextView mFirstName, mLastName;
public void setFirstName(@NonNull final String firstName) {
mFirstName.setText(firstName);
}
public void setLastName(@NonNull final String lastName) {
mLastName.setText(lastName);
}
public void setAge(@IntRange(from = 1) int age) {
mAge = age;
}
...
}
只要有 setter 方法就可以像下面代码一样赋值:
onClickListener 也是同样道理(因为任何View都有对应的setOnClickListener方法),只不过我们是在 Activity 中定义了一个 Listener。
重命名Setters
- BindingMethod:Used within an BindingMethods annotation to describe a renaming of an attribute to the setter used to set that attribute. By default, an attribute attr will be associated with setter setAttr.
- BindingMethods:Used to enumerate attribute-to-setter renaming. By default, an attribute is associated with setAttribute setter. If there is a simple rename, enumerate them in an array of BindingMethod annotations in the value.
@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
Class type();//The View Class that the attribute is associated with.
String attribute();//The attribute to rename. Use android: namespace for all android attributes or no namespace for application attributes.
String method();//The method to call to set the attribute value.
}
@Target({ElementType.TYPE})
public @interface BindingMethods {
BindingMethod[] value();
}
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
...
})
public class TextViewBindingAdapter { ... }
自定义Setters
@BindingAdapter注解简介
@Target(ElementType.METHOD)
public @interface BindingAdapter {
String[] value();//The attributes associated with this binding adapter.
boolean requireAll() default true;
}
@BindingAdapter("android:bufferType")
public static void setBufferType(TextView view, TextView.BufferType bufferType) {
view.setText(view.getText(), bufferType);
}
@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);//再添加新的
}
}
@BindingAdapter({"android:onClick", "android:clickable"})
public static void setOnClick(View view, View.OnClickListener clickListener, boolean clickable) {
view.setOnClickListener(clickListener);
view.setClickable(clickable);
}
系统提供的示例
public class TextViewBindingAdapter {
@BindingAdapter({"android:bufferType"})
public static void setBufferType(TextView view, TextView.BufferType bufferType) {
view.setText(view.getText(), bufferType);
}
private static void setIntrinsicBounds(Drawable drawable) {
if (drawable != null) drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
@BindingAdapter({"android:drawableBottom"})
public static void setDrawableBottom(TextView view, Drawable drawable) {
setIntrinsicBounds(drawable);//设置边界
Drawable[] drawables = view.getCompoundDrawables();
view.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawable);//修改DrawableBottom
}
}
自定义案例一
android:paddingTop="@{pt}"
@BindingAdapter({"android:paddingTop"})
public static void setPaddingTopAndBottom(View view, int paddingTB) {//当设置paddingTop时,同时设置paddingTop和paddingBottom
view.setPadding(view.getPaddingLeft(), paddingTB, view.getPaddingRight(), paddingTB);
}
mBinding.setPt(10 * new Random().nextInt(10));
自定义案例二
@BindingAdapter({"bind:imageUrl", "error"})//自定义 namespaces 将被忽略
public static void loadBqtImage(ImageView view, String url, Drawable error) {//方法名随意,但参数必须和注解中指定的属性一一对应
Picasso.with(view.getContext()).load(url).error(error).into(view);
}
- 匹配过程中自定义 namespaces 将被忽略,你可以在@BindingAdapter中加任何 namespaces,但是会有如下提示:
Error:(37, 21) 警告: Application namespace for attribute bind:imageUrl will be ignored.
- 允许重写android的命名空间。
- 当你创建的适配器属性与系统默认的产生冲突时,你的自定义适配器将会覆盖掉系统原先定义的注解,这将会产生一些意外的问题。
转换器 @BindingConversion
对象转换
@BindingConversion注解简介
@Target({ElementType.METHOD})
public @interface BindingConversion {
}
自定义转换
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
/**
* !!! Binding conversion should be forbidden, otherwise it will conflict with{@code android:visiblity} attribute.
*/
@BindingConversion
public static int convertColorToString(int color) {
switch (color) {
case Color.RED:
return R.string.red;
case Color.WHITE:
return R.string.white;
}
return R.string.app_name;
}
2017-9-27