2016-08-12 13:00
Android DataBinding 是google在android上对MVVM设计模式的一个实现框架,目前还在测试阶段,稳定性,性能和兼容性还存在一些问题,所以目前在稳定版本的App上应用的还很少。
Android DataBinding主要实现了View和ViewModel的双向绑定,包括用户的响应。并且实现了自动更新。
Android DataBinding是一个support library,可以兼容android2.1(API Level 7)及以上的版本,开发需要使用Gradle1.5.0-alphal或更高版本的插件
开发环境
在你的module中的build.gradle文件中添加dataBinding配置:
android {
....
dataBinding {
enabled = true
}
}
添加完成之后,使用gradle同步项目,会自动下载支持类库。
布局
<?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>
Android DataBinding的布局文件必须与<layout>标签为起点。
<data>标签内就是这个布局要绑定的数据。
variable表示一个可能会在这个布局中作用的属性。
一个<data>标签内可以包含多个variable标签,即一个布局可以绑定多个数据对象类
variable的name就是这个布局中引用该数据对象的名称,type为该数据对象对应的类,通常为包名+类名
布局属性的设置使用“@{ }”语法
数据对象
POJO:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
常在应用程序的数据读取一次,不会改变.还可以改为javabean对象:
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;
}
}
从数据绑定的角度来看,这两个类是没什么区别的。用于设置TextView的android:文本的表达式@{user.firstName}将访问第一个类中的firstName字段和后一个类的getFirstName()方法
另外,如果对象中firstName()方法存在,它还将访问firstName()方法。
数据绑定
@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);
}
默认情况下,绑定类的名称是基于布局文件的名称生成的,它是将布局文件名开头大写并加上“Binding”而成。这个类拥有所有从属性(例如用户变量)到布局的绑定关系并知道如何赋值绑定表达式。最简单的方法创建绑定的方法就是通过反射。
事件处理
事件出来主要是处理用户交互的事件响应,如点击,长按,控件的拖动等,有两种方式来实现事件处理:
Method References: 和android的现在的方式类似,在layout文件中绑定事件对应的方法。
如:
public class MyHandlers {
public void onClickFriend(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="@{handlers::onClickFriend}"/>
</LinearLayout>
</layout>
Listener Bindings: 和Method References很相似,不过可以使用lambda表达式传递任意的数据。不过只有Gradle 2.0及以上才支持。
如:
public class Presenter {
public void onSaveClick(Task task){}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
</data>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
<Button android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="@{() -> presenter.onSaveClick(task)}" />
</LinearLayout>
</layout>
如果点击事件执行的方法中要使用当前的View,也可以通过参数传过去:
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
在lambda表达式中可以传递多个参数。如果表达是中参数无NULL,Data Binding会返回默认个的值。表达式中还可以使用void的关键字
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
避免复杂的listener表达式,会使代码难以阅读和维护,表达式应该只传递有效的参数。
布局细节
1.import
import和Java的import的功能类似,在layout文件中使用该标签导入对应的类就可以使用该类的属性和方法
<data>
<import type="android.view.View"/>
</data>
在布局中就可以使用view的属性和方法
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
导入类型时也可以使用类型的静态方法和属性:
<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"/>
变量:
在<data>标签中可以声明多个不同类型的变量
<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>
在编译时会检查引入的变量,如果一个变量继承了Observable或Observable集合对象,那会通过反射得到,如没有实现Observable接口,则数据的变化不会被观察。
2.自定义绑定类型名称
绑定类可以被重命名或放在不同的包中通过class标签
<data class="ContactItem">
...
</data>
生成绑定类ContactItem将会存在module包下的databinding包中。如果想让生成的类放在module中的另一个包下,可以加入前缀:
<data class=".ContactItem">
...
</data>
也可以使用包名的全称:
<data class="com.example.ContactItem">
...
</data>
3.includes
和layout的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>
<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>
merge标签不支持include数据绑定
表达式语法
表达式语法跟java语法很像,下面的一样的语法:
· 数学计算 + - / * %
· 字符串连接 +
· 逻辑运算符&& ||
· 位运算& | ^
· 一元运算 + - ! ~
· 位移>> >>> <<
· 比较== > < >= <=
· instanceof
· Grouping ()
· 文字 - character, String, numeric, null
· Cast
· 方法调用
· 字段访问
· 数组访问 [ ]
· 三元运算符?
如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
不支持的:
· this
· super
· new
· Explicit generic invocation
NULL 判断:
android:text="@{user.displayName ?? user.lastName}"
与下面的一致:
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
4.集合:
array:
<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]}"
Map:
由于Map的key为字符串,在XML中引用map的子项也是双引号,所以可以使用单引号和`:
android:text='@{map["firstName"]}'
//or
android:text="@{map[`firstName`}"
android:text="@{map["firstName"]}"
5.Resources:
与布局一致,可以使用表达式:
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)}"
6.数据对象
要使数据对象可监控,必须实现Observable接口,有三种不同的可观察的数据机制:Observable objects, observable fields, and observable collections.
· Observable objects:
一个可以监控对象的接口实现,可以把监听器和对象绑定,可以监听所有属性的变化.要实现一个可监控数据对象,该对象(POJO)需继承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类,放在module的包下.
数据发生改变时,应该在setter方法用调用notifyPropertyChanged方法来通知监听器。
· ObservableFields:
如果一个类中只有几个变量需要监控,那可以不集成BaseObservable类,使用ObservableFields来监控。
一些基本类型的字段监控类: ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable.
ObservableFields是一个独立的可监控对象,只能监控一个属性。原始的版本使用时为了避免装箱和拆箱,使用public final 修饰
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();
· Observable Collections:
ObservableArrayMap:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
layout:
<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:
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"/>
7.Views With IDs
当一个Layout中的view声明了ID时,会自动生成对应的public final变量。数据绑定通过View的结构提取出有ID的view。这种机制比调用findViewById更高效
<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="@+id/lastName"/>
</LinearLayout>
</layout>
数据绑定在上面的布局下将会自动生成下面的变量:
public final TextView firstName;
public final TextView lastName;
Variables:
每个layout中声明的Variables对象,都应该声明对应的操作方法:
<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>
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);
8.ViewStubs
ViewStub和常规的View不太一样,它开始的时候是不可见的,可以延迟到运行时填充布局资源。
由于ViewStub会在视图层消失,为了正常的连接,对应的绑定对象也要随之消失。由于视图层是final类型的,ViewStubProxy对象替代ViewStub 后,开发者可以访问 ViewStub,并且当 ViewStub 在视图层中被加载时,开发者也可以访问加载的视图。
当ViewStub填充其他布局时,新的布局中要建立绑定关系。因此,ViewStubProxy 对象要监听ViewStub的OnInflateListener并建立绑定。开发者可以在ViewStubProxy 对象上建立一个OnInflateListener,绑定建立后,便可调用OnInflateListener。
高级绑定
1,动态变量
布局有时候要绑定的数据对象是不确定的,例如:RecyclerView.Adapter,必须在onBindViewHolder中指定绑定值,才可以识别出对应的绑定类。
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
上面这个例子中,RecyclerView绑定的所有layout有一个item变量,BindingHolder有一个getBinding方法来获取ViewDataBinding对象
· 立即绑定
当一个变量或Observable对象发生变化时,绑定应该在下一帧之前按序进行改变,但是有时候需要立即执行绑定,可以调用 executePendingBindings()方法强制执行绑定
· 后台线程:
可以改变你的数据对象在后台线程,除了集合。数据绑定将会本地化保存每一个变量/字段,防止并发错误
Attribute Setters:
当一个绑定的值发生变化时,自动生成的数据绑定对象必须调用对应的setter方法在界面上赋值,数据绑定框架有几种方法去自定义绑定的方法
1.自动Setters
数据绑定会自动寻找参数对应的setter方法,跟命名空间无关,只跟属性的名称相关。
例如:textview 在不居中通过android:text赋值之后,会自动寻找setText(String)方法,如果表达式返回的是Int类型,熟悉绑定会自动寻找setText(Int)方法。所以要确保表达式返回的类型正确。若需要可以转换。即使没有给定的属性,数据绑定也会执行。可以通过数据绑定调用setter方法创建属性。例如,DrawerLayout没有任何属性,但有很多setter方法,可以调用setter方法自动创建属性。
android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
2.重命名Setters
一些属性的setter方法与名称不一致,可以通过BindingMethods注解关联名称和方法。
例如:可以将android:tint 属性与setImageTintList(ColorStateList)方法相关联
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
开发者不需要重命名setter方法,数据绑定框架已经实现。
3.自定义setter
有一些参数需要自定义绑定逻辑,如:android:paddingLeft属性没有setter方法,但是有一个setPadding(left,top,right,bottom)方法,@BindingAdapter可以自定义怎么绑定该属性
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
BindingAdapter是很有用的去自定义别的属性,例如,自定的加载器可以利用空线程加载图片。
当适配器冲突的时候,开发者定义的BindingAdapter将覆盖默认的适配器。
一个适配器也可以绑定多个属性
@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}"/>
绑定适配器的方法可能需要持有旧的值,采用旧值和新值的方法应该首先具有属性的所有旧值,然后是新值,
@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);
}
}
}
当一个监听器有多个方法的时候,必须拆分进多个监听器。例如 View.OnAttachStateChangeListener有两个方法:
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}
因为改变一个监听器方法的时候会影响到另一个,所以要提供三个绑定适配器,添加一个适配器来同时处理两个事件:
@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);
}
}
}
android.databinding.adapters.ListenerUtil类用来帮助跟踪之前的监听器,保证之前的监听器被移除
3.转换器
当一个对象从绑定表达式中被返回,对象的setter方法将会从自动,重命名和自定义setter中选择,对象会被转化为setter指定的类型
例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap 是一个ObservableMaps,userMap返回一个对象,对象将会被转化为字段对应的类型在setter方法中setText(CharSequence)。当参数类型不明确的时候,开发者应该在表达式中转化
自定义转化器
有时候转换器应该自动转化两个特殊类型的对象,如:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
background参数类型是Drawable,而color是Int,无论预期的是Drawable还是int类型,int类型都应该被转换为colorDrawable.
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
注意:转化只能发生在setter级别,所以不允许在表达式中使用混合类型:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Android studio对DataBinding的支持
Android studio支持DataBinding的多种代码边距功能,例如:
语法高亮显示,在编辑器中显示语法错误,xml代码自动补全,引用,代码源码导航。
布局预览也支持在表达式中设置默认值,例如:
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=PLACEHOLDER}"/>
在预览页面中该TextView的文本将会显示为PLACEHOLDER。如果在设计时需要显示不同的值,也可以使用tools参数代替表达式中的default值。
网友评论