一、Data Binding是什么?
2015年的Google IO大会上,Android 团队发布了一个数据绑定框架(Data Binding Library),Data Binding实现了 MVVM 模型中的ViewModel。以后可以直接在 layout 布局 xml 文件中绑定数据了,无需再 findViewById 然后手工设置数据了。其语法和使用方式和 JSP 中的 EL 表达式非常类似。
Data Binding Library 是一个 support 库,支持 Android 2.1+ 版本 (API level 7+)。 由于该框架需要使用编译器来生成很多代码,所以需要配合新版本的 Android Studio (1.3.0-beta1 + 版本)才能使用,Gradle 插件1.5.0-alpha1以上。
二、配置环境
Android Studio版本是1.3+,更新Support repository到最新的版本。
新建一个project,在dependencies中添加以下依赖classpath "com.android.databinding:dataBinder:1.0-rc1"
新建module,并且在module的build.gradle文件中添加
apply plugin: 'com.android.application' apply plugin: 'com.android.databinding'
Studio1.5+忽略以上两点,只需在对应module的build.gradle中添加
android { .... dataBinding { enabled = true } }
ps:即使依赖的库中使用了data binding,该module也必须在build.gradle中配置
三、初体验
3.1 布局文件
<?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>
3.2 数据对象
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;
}
}
3.3 绑定数据
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
// or
// MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
User user = new User("Test", "User");
binding.setUser(user);
}
在 ListView 或者 RecyclerView 的Adapter中使用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
3.4 绑定事件
如果你的数据对象中有两个方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(View view) { ... }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="handlers" type="com.example.Handlers"/>
<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:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"
android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
</LinearLayout>
</layout>
四、详细说明
4.1 导包
<data>
<import type="android.view.View"/>
</data>
…
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
像java一样,java.lang.*
自动导入,可以直接使用
当存在同名的类时,其中一个可以使用别名
<import type="android.view.View"/>
<import type="com.example.real.estate.View" alias="Vista"/>
导入类还可以用于在表达式中引用静态属性和方法
<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"/>
4.2 变量
自动生成的binding类会为每个声明的变量生成setter和getter方法,在调用setter方法之前变量的值为java中对应类型的默认值。
当表达式中需要使用时,会自动生成一个名为“context”的变量,“context”的值是由根布局的getContext()
获得,如果手动声明了一个名为“context”的变量,默认的会被覆盖。
4.3 自定义Binding类名
默认Binding类会根据布局文件名自动生成,放在module包下的databinding包中,如布局文件contact_item.xml
会自动生成ContactItemBinding,如果module包名是com.example.my.app
,这个类将被放在com.example.my.app.databinding
下。通过class属性,可以修改Binding类的类名和所在包。
databinding包下,类名为ContactItem
<data class="ContactItem"> ... </data>
module包下
<data class=".ContactItem"> ... </data>
指定包名
<data class="com.example.ContactItem"> ... </data>
4.4 Includes
使用include时,需要将变量传递到被包含的布局中,下面例子中 name.xml
和 contact.xml
中必须有user变量。
<?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>
Data binding不支持<merge>
下直接的子元素使用include,比如下面的就不支持:
<?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>
<merge>
<include layout="@layout/name"
bind:user="@{user}"/>
<include layout="@layout/contact"
bind:user="@{user}"/>
</merge>
</layout>
五、表达式
5.1 普通
以下表达式和java的一样
* 算术表达式 + - / * %
* 字符串连接 +
* 逻辑运算符 && ||
* 位运算符 & | ^
* 一元运算符 + - ! ~
* 位移运算符 >> >>> <<
* 关系运算符 == > < >= <=
* instanceof
* 分组 ()
* 字面值 - 字符, 字符串, 数字, null
* 类型转换
* 方法调用
* 访问成员变量
* 访问数组 []
* 三元运算符 ?:
例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
5.2 缺少的
java中可以使用而这里不能使用的
* this
* super
* new
* Explicit generic invocation
5.3 判断非空运算符
当表达式左边不为null时使用左边的值,如果为null使用右边的
android:text="@{user.displayName ?? user.lastName}"
等效于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
5.4 避免了NullPointerException
生成的data binding代码会自动检查空值,避免空指针。例如表达式@{user.name}
,如果user
是null
,user.name
将使用默认值null
,user.age
将使用默认值0。
5.5 集合(Collections
)
集合使用泛型时小于号
<
需要用<
代替
<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]}"
键值为字符串时
android:text='@{map["firstName"]}' <!-- 边上是单引号 -->
android:text="@{map[`firstName`}" <!-- 中间是反单引号(数字键1左边) -->
android:text="@{map["firstName"]}"
5.6 资源文件(Resources
)
直接在表达式中使用resources
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化带参数的字符串
android:text="@{@string/name(name)}" android:text="@{@string/name_and_gender(name, gender)}"
<!-- s是字符串,d是整数 --> <string name="name">姓名:%s</string> <string name="name_and_gender">姓名:%1$s;性别:%2$s</string>
格式化带参数的plural,使用较少,这里不做过多解释
Have an orange Have %d oranges android:text="@{@plurals/orange(orangeCount, orangeCount)}"
有些
resource
必须明确指出类型类型 普通引用 表达式引用 String[] @array @stringArray int[] @array @intArray TypedArray @array @typedArray Animator @animator @animator StateListAnimator @animator @stateListAnimator color int @color @color ColorStateList @color @colorStateList sample
android:text="@{@stringArray/genders[1]}" android:text="@{String.valueOf(@intArray/ages[2])}"
<array name="genders"> <item>男</item> <item>女</item> </array> <array name="ages"> <item>11</item> <item>12</item> <item>13</item> </array>
六、数据对象
任何一个简单的Java对象(POJO)都可以用来绑定,但是修改一个POJO不能够触发UI更新。
当Observable objects, observable fields, 和 observable collections 这3种数据对象绑定到UI,并且数据改变时UI会自动更新
6.1 Observable Objects
实现 android.databinding.Observable
接口。为了方便,Android提供了基类BaseObservable
,实现了监听器的注册机制。
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
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是在编译时生成在module的一个类,功能与R.java类似。
6.2 ObservableFields
属性较少时可以使用ObservableField
,包含ObservableBoolean
, ObservableByte
, ObservableChar
, ObservableShort
, ObservableInt
, ObservableLong
, ObservableFloat
, ObservableDouble
, 和 ObservableParcelable
,简单的POJO就可以实现。
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
访问值时使用:
user.firstName.set("Google");
int age = user.age.get();
6.3 Observable Collections
6.3.1 ObservableArrayMap(当key是引用类型时)
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
<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"/>
6.3.2 ObservableArrayList(当key是整数时)
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<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
生成的binding类引用了layout中的View,就如前面说的,Binding的类名和包名都可以定制,生成的binding类都继承自ViewDataBinding
。
7.1 创建
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
If the layout was inflated using a different mechanism, it may be bound separately:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
7.2 带ID的View
如果布局中某个View设置了id,则生成的binding类中会包含:
public final TextView firstName;
public final TextView lastName;
使用时直接调用 binding.firstName
7.3 ViewStubs
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
...>
<ViewStub
android:id="@+id/view_stub"
android:layout="@layout/view_stub"
... />
</LinearLayout>
</layout>
binding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
ViewStubBinding binding = DataBindingUtil.bind(inflated);
User user = new User("fee", "lang");
binding.setUser(user);
}
});
7.4 高级用法
7.4.1 动态变量
以 RecyclerView 为例,Adapter 的 DataBinding 需要动态生成,因此我们可以在 onCreateViewHolder 的时候创建这个 DataBinding,然后在 onBindViewHolder 中获取这个 DataBinding。
@Override
public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.bind(LayoutInflater.from(parent.getContext())
.inflate(layoutId, parent, false));
BindingHolder bindingHolder = new BindingHolder(binding.getRoot());
bindingHolder.setBinding(binding);
return bindingHolder;
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
// 当一个变量改变时,binding会在下一帧时改变UI,需要立刻执行,可以使用executePendingBindings()方法。
holder.getBinding().executePendingBindings();
}
public static class BindingHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;
public BindingHolder(View itemView) {
super(itemView);
}
public ViewDataBinding getBinding() {
return binding;
}
public void setBinding(ViewDataBinding binding) {
this.binding = binding;
}
}
八、Attribute Setters
当一个绑定的值改变时,可以指定调用哪个方法来设置值
8.1 Automatic Setters
没有配置自定义属性,data binding会自动找到对应的setAttribute
方法(命名空间无所谓),注意表达式的返回值,必要的时候进行类型转换。
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
8.2 Renamed Setters
如果不想调用根据名字匹配的setter,可以通过BindingMethods
注解重新匹配。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
通常开发者不需要重命名setters
8.3 Custom Setters
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
还可以接收多个参数
@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);
}
<ImageView app:imageUrl="@{venue.imageUrl}"
app:error="@{@drawable/venueError}"/>
这个适配器会在imageUrl和error都在ImageView中设置,imageUrl是字符串,error时drawable时调用
Binding adapter methods may optionally take the old values in their handlers. A method taking old and new values should have all old values for the attributes come first, followed by the new values:
@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);
}
}
}
当一个listener有多个方法时,必须分成多个listener。比如View.OnAttachStateChangeListener
有两个方法:onViewAttachedToWindow()
和 onViewDetachedFromWindow()
,就必须创建两个接口来区分他们。
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为改变一个listener经常会影响另一个,所以我们需要有3个不同的绑定适配器。
@BindingAdapter("android:onViewAttachedToWindow")
public static void setListener(View view, OnViewAttachedToWindow attached) {
setListener(view, null, attached);
}
@BindingAdapter("android:onViewDetachedFromWindow")
public static void setListener(View view, OnViewDetachedFromWindow detached) {
setListener(view, detached, null);
}
@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"})
public static void setListener(View view, final OnViewDetachedFromWindow detach,
final OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
final OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}
final OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view,
newListener, R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}
上面的例子比普通的稍微复杂一些,因为View使用add和remove来操作listener,不使用set方法来操作View.OnAttachStateChangeListener
。The android.databinding.adapters.ListenerUtil
class helps keep track of the previous listeners so that they may be removed in the Binding Adaper.
By annotating the interfaces OnViewDetachedFromWindow
and OnViewAttachedToWindow
with @TargetApi(VERSION_CODES.HONEYCOMB_MR1)
, the data binding code generator knows that the listener should only be generated when running on Honeycomb MR1 and new devices, the same version supported by addOnAttachStateChangeListener(View.OnAttachStateChangeListener)
.
九、转换器(Converters)
9.1 Object Conversions
当binding表达式返回一个对象时,会从automatic, renamed, 和 custom setters 中选择一个,这个对象会被转换成选择的setter的参数类型。
这方便了用ObservableMaps保存数据,例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap
返回了一个 Object ,这个 Object 将会自动转换成 setText(CharSequence)
的参数类型。当参数类型不明确时,开发者需要在表达式中进行转换。
9.2 Custom Conversions
有时特殊的类型需要自动转换。比如,当设置背景时:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这里,背景是一个 Drawable
,但是颜色是一个整数,需要把 int
转换成 ColorDrawable
,可以使用带有 BindingConversion
注解的一个静态方法:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
Note:不要将上面的和以下混淆
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>