一个刚入行半年的菜鸟安卓开发人员,始终有一颗不安分的心。
mvvm框架是我在学习vue的时候才知道的一种新型架构。公司项目从开始到现在都是由我一个人完成的安卓客户端,在不断的踩坑过程中,我放弃了学习mvp架构,采用mvc架构开发的程序。而后在学习http过程中我认识了js,认识了node,同时知道了vue,一个新的架构mvvm让我知道了代码的简洁之道,我决定从新构建我的app,采用mvvm架构。
mvvm是采用model-viewmodel-view组成的,在我目前的理解中,model是自己的模型,view是布局文件,使用databinding后布局文件支持一些简单的逻辑表达式和简单的判断,viewmodel一般在我的项目中被指向activity、fragment和自定义view。
关于databinding网上已经有很多教程了,这篇文章是这两天翻译的官方文档,有点长,请同学们认真读完。
Databinding 库
这篇文档说明了这样使用DataBinding库来编写声明式布局并且精简应用程序逻辑和布局所需的粘性代码。
DataBinding库提供了灵活性和广泛的兼容性。它是一个支持库,你可以在安卓平台2.1(Api 7+)都可以使用这个库。
使用Databinding库要求安卓的gradle插件最低是1.5.0.
构建环境
在安卓SDK管理工具中的支持库中下载DataBinding库来开始在你的项目中使用它。
在你app级别的build.gradle文件中加入DataBinding元素,来在你的应用重配置DataBinding。
代码如下
android {
....
dataBinding {
enabled = true
}
}
如果你的应用中其他模块也使用了DataBinding库,你同样需要在对应的模块中的build.gradle中加入以上代码。
确保你的Android Studio版本在1.3之上。
DataBinding的布局文件
编写你的第一个DataBinding表达式
DataBinding的布局文件和原来的布局稍有些不同,它的根布局是一个layout标签,里边包含一个data标签和原来的根布局。
<?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>
在data标签中的user变量在你的布局文件中使用。
<variable name="user" type="com.example.User"/>
布局文件中的表达式要写在属性中,并写成”@{}”形式。这个例子中,TextView中的文字被设置成为了user的firstName属性。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
数据对象
让我们假设你有一个普通的java对象:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这个对象中的数据不会改变。类似于这样的对象在java中非常常见并且可能被应用于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;
}
}
从DataBinding的角度来看,这两个对象是相等的。在TextView属性中的android:text使用的表达式@{user.firstName}使用的是后者中的getFirstName方法。或者它也会被使用firstName()方法如果这个方法存在。
绑定数据
通常情况下系统会生成一个Binding类,类的名称是根据你的布局文件后加上”Binding”。上边的布局文件名称是’main_activity.xml’,生成的类的名称是MainActivityBinding。(这里为了不引起误会我简单说一下,通常MainActivity生成的布局文件一般是activity_main.xml,则此时生的Biding类的名称应该是ActivityMainBinding)。这个类持有布局中所有属性的值(如user的firstName)并且知道如何通过表达式分发到各个控件。创建绑定控件的最简单的方式是在inflating的时候。
@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);
}
这样就完成了DataBinding的数据绑定,现在运行你应用程序就可以得到你设置的结果。你也可以这样:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你正在ListView或者RecyclerView的Adapter中使用,你可以这样做:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
传递事件
DataBinding允许你为控件通过写表达式来传递事件,表达式必须是控件分发事件(例如 onClick)。 事件名称必须使用官方的监听方法的名称。 例如View.OnLongClickListener有一个方式是onLongClick(),所以这个事件的属性必须是:android:onLongClick.这里有两条途径来传递事件:
- 方法引用:在你的表达式中你可以直接引用监听器的方法。当你引用方法时,DataBinding将会将引用方法和它所属的类包裹起来,并指定给目标控件。如果表达式的值为空,DataBinding不会创建方法并指定一个空的listener给控件。
- 监听绑定:使用lambda表达式来绑定事件。DataBinding也会创建一个监听器并指定给控件。当事件分发时,监听器会解析lambda表达式。
方法引用
事件可以在直接用方法来传递。在Activity中也可以绑定类似于android:onClick方法,但是相对来说在布局文件中使用的好处就是可以在在编译的时候对表达式检查,如果有错误或者空指针会立即报错。
方法引用和监听器绑定的主要区别是,在实际运行的时候,监听器绑定在数据创建时绑定的,而不是在在事件触发时创建的。如果你喜欢在事件触发时创建,可以用监听器绑定的方法。
为了使事件正常分发,你的绑定表达式必须和所对应的回调方法有一样的参数。例如你的数据对象有两个方法:
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>
Note:方法的表达式必须和安卓系统的给定的表达式一致。
监听绑定
监听绑定是在事件触发时才进行绑定。和方法引用类似,但是它允许你在运行时传递任意参数。这个特性要求Android Gradle Plungin的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>
监听器只允许作为表达式根元素的lambda表达式来表示。当在表达式中使用回调方法时,DataBinding自动为这个事件创建了一个监听器和寄存器。当控件触发时事件时,DataBinding会重新转换给定的表达式。在常规表达式中,当这些监听器表达式被转换时可能会得到空值和线程安全的DataBinding。
Note:在上边的例子中,我们没有定义传递给onClick(android.view.view)的参数view。监听绑定可以有两个参数,你可以忽略他们或者全部给他们命名。如果你想要给全部参数命名,你可以在你的表达式中如下写:
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
你的数据对象中应该如下定义:
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
你也可以使用lambda表达式来传递更多参数
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
如果你监听的事件返回一个非空的值,你的表达式必须返回相同的类型。例如你想要监听longCLick事件,你的表达式应该返回一个boolean值。
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
假如由于空对象导致的表达式不能被转换,DataBinding会返回对应的默认的类型。例如0对于int,false对于boolean,null对于引用类型。
避免使用复杂的监听器
监听器表达式非常强大,可以是你的代码可读性更好。另一方面,包含复杂表达式的监听器让你的布局文件难以阅读和理解。这些表达式从UI传递到回调方法中的参数应该越简单越好。你应该在监听器的表达式调用方法中实现所有的业务逻辑
一些特定的点击事件已经存在并且需要android:onClick来避免冲突。下列属性已经被创建来避免冲突:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局细节
import元素在data标签中使用,这样可以让你像在java代码中导入你的类文件。
<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}"/>
当存在类的名称冲突时,可以使用别名来解决:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
现在在布局文件中你就可以使用Vista来表示com.example.real.estate.View,用View来表示android.view.View.在变量和表达式中也可以使用导入的类型。
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
Note:AndroidStudio目前还不支持为DataBinding的自动补全。但是你的应用还是会正常工作。你也可以使用完整的包名。
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
导入的类型也可以加载静态方法或者表达式中的方法。
<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"/>
就像Java中的import方法。
变量
在data标签种可以使用任意数量的variable元素。在布局文件中每一个variable元素都描述了一个在binding表达式中的属性。
<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>
变量类型是在编译的时候进行检查的,如果一个变量继承了Obserable或者是一个Obserable集合,应该在类型上反应出来。假如变量是一个没有继承Obserable的基础类或者接口,这个变量将不会被观察。
当有不同的布局文件配置时(例如landscape和portrati),这些变量将会合并。不允许出现可能会产生冲突的变量定义。
生成的binding类将会为每一个variable生成一个getter和setter方法,在文件未调用setter方法时,variable将会使用默认值–
null对于引用类型,0对于int,false对于boolean。
一个叫做context的特殊变量会生成,用于绑定表达式中。contex的值是根view的getContext(),context将会被同样名字的显示变量覆盖。
自定义绑定类的名称
默认情况下Binding类的名字是根据布局文件生成的,首写字母大写,下划线(_)去除,并将紧随其后的第一个字母大写然后加上Biding。这个类在module下的databinding包里边。例如,布局文件的名称是contact_item.xml,则生成的binding类的名称是ContactItemBinding,假如包的名称是com.example.my.app,则生成的类所在的位置是com.example.my.app.databinding.
Bindding类可以通过使用属性来重命名或者放在指定位置
<data class="ContactItem">
...
</data>
上面一段代码在包中生成的binding类的名称是ContactItem。如果想在其他的包中生成这个类可以加上前缀”.”。
<data class="com.example.ContactItem">
...
</data>
Includes标签
Variables可以通过应用的命名空间和variable名字属性从布局文件传递给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>
同样的,在include布局中应该有一样的vardable。
DataBinding不支持直接包含一个merage的布局,例如:
<?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>
表达式语言
常用特性
表达式语言和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 ?:
例如:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
不支持的操作
java中的一些特性在DataBinding语法中不支持
- this
- super
- new
- Explicit generic invocation
空合并运算(Null Coalescing Operator)—不知道怎么翻译
空合并运算(??)后边跟两个值,假如左边的值不为空会取左边的值,左边的值为空就去右边的值。
android:text="@{user.displayName ?? user.lastName}"
等于下面的表达式
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
前文中提到过,使用的是类中的getter方法或者是Observable域。
android:text="@{user.lastName}"
避免空指针异常
生成的DataBinding类会自动检查代码中的空对象并避免空指针异常。例如,表达式中@{user.name},假如user是空对象user.name将会是默认的值null。假如你在使用user.age,而这个age是一个int型,则会自动重置为0.
集合
常用的集合类型: arrays, lists, sparse lists,和maps, 可以通过简单的使用[]来表示。
<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`}"
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 |
数据对象
在DataBinding中可以使用任意的java对象,但是这些对象改变时不会引起UI的改变。DataBInding的强大之处正是可以赋予使你的数据对象通知UI改变的能力。有三种不同的数据改变通知机制:Observable objects, observable fields, 和observable collections.
当在UI和属性中使用了上面三种的一种时,UI会自动更新数据。
Obserable接口有一个自动添加和移除监听器的机制,但是通知数据更新取决于开发者。为了使开发变得简单,谷歌创建了BaseObserable这个基础类来集成监听器注册机制。通过给getter方法添加Bindable注解,通知setter方法。
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.class中生成一个实体,这个BR.class会在包中生成。如果数据对象的基类不能被改变,则Observable接口可以使用简单的PropertyChangeRegistry来实现,以有效地存储数据和通知监听器。
ObservableFields
创建Observable类会稍微有一点麻烦,所以如果开发者想要节省时间可以使用ObservableField和它的同代-bservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 ObservableParcelable。ObservableFields拥有一个自己的额Observable域,避免在访问操作期间装箱拆箱。在使用时请在类中创建一个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();
}
可以使用get和set方法来改变值。
user.firstName.set("Google");
int age = user.age.get();
Observable Collections
在一些应用中会使用动态结构来持有数据。Observable collections 允许通过键来访问对应的值。ObservableArrayMap键是字符串类型时非常有用,例如:
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中,通过字符串的key可以获得值:
<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在键是int类型时非常有用:
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类将布局文件中的variable与视图关联起来,像前边说的,Binding类的名称可以自定义,生成的Binding类继承了ViewDataBiding。
创建
应在inflation后立即创建绑定,以确保在使用布局中的表达式绑定到视图之前,View层次结构不受干扰。 有几种方法绑定到布局。 最常见的是在Binding类上使用静态方法。inflate方法扩充了View层次结构并且绑定到它所有一步。 有一个更简单的版本,只需要一个LayoutInflater和一个还需要一个ViewGroup:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果布局文件使用一种不同的机制,可以单独绑定:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时,binging类不能提前知道。 在这种情况下,可以使用DataBindingUtil类创建绑定:
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
带Id的Views
在布局文件中带Id的View会创建出一块public final域,绑定在View层次结构上执行单次传递,从而使用ID提取视图。 这个机制可能比为几个视图调用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>
生成一个binding类包含以下内容:
public final TextView firstName;
public final TextView lastName;
Id在数据绑定中几乎不会使用,但是仍然有一些情况下需要通过id获取视图。
变量
每一个变量都应该有访问方法。
<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>
在绑定时自动生成了getter和setter方法
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(这篇不常用, 而且文档介绍较少)
ViewStub和普通的view 有一些区别,开始的时候它们是不可见的,知道被设置为可见或者inflate,它们会将自己替换为另个一个指定的布局。
因为ViewStub基本上再View层次结构中不可见,所以绑定对象中的View也必须消失以允许收集。 因为View是final的,所以ViewStubProxy对象代替ViewStub,当ViewStub存在时允许开发人员访问ViewStub,当ViewStub替换后,还可以访问替换后的的View层次。
替换为另一个布局时,必须为新布局建立绑定。 因此,ViewStubProxy必须监听ViewStub的ViewStub.OnInflateListener并在那时建立绑定。 由于只有一个可以存在,ViewStubProxy允许开发人员设置一个OnInflateListener,它将在建立绑定后调用。
高级绑定
动态变量
有时候,不知道一些特殊的绑定类。例如,对任意布局的RecyclerView.Adapter不会知道特定的绑定类。 它仍然必须在onBindViewHolder(VH,int)期间分配绑定值。
在这个例子中,所有绑定的条目都有一个变量,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发生变化时,绑定将会在下一帧改变,但是有时候必须立即执行,此时需要调用xecutePendingBindings()方法。
后台线程
您可以在后台线程中更改数据模型,只要它不是集合即可。 数据绑定将本地化每个变量/字段,同时评估以避免任何并发问题。
属性设置
每当绑定值更改时,生成的绑定类必须在具有绑定表达式的视图上调用setter方法。 数据绑定框架有自定义哪个方法来调用以设置值的方法。
Automatic Setters
对于一个属性来说,DataBinding视图查找设置该属性的方法,属性的命名空间无关紧要,只有属性本身有关系。例如:TextView的android:text属性会查找setText(String)方法。如果表达式返回一个int值,则DataBinding会查找setText(int)方法。在表达式返回的类型这件事上要特别小心,如果需要请使用强制转换。注意,DataBinding在被给定的属性不存在时仍会工作。你可以轻松的创建任何属性。例如。DrawerLayout没有任何属性,你可以使用自动创建setter方法设置如下属性:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
重命名setter
一些属性的setter名不符实,你可以通过BindingMethods注解与setter关联。这必须与一个类相关联,并包含BindingMethods注解。例如,android:tint属性确实与setImageTintList(ColorStateList)相关联,而不是setTint。
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
开发者不必去重命名setters,安卓框架会完成这些工作。
自定义setters
一些属性需要自定义绑定的逻辑,例如:系统没有为android:paddingLeft属性的关联setter。 相反,setPadding(left,top,right,bottom)存在。 使用BindingAdapter注释的静态绑定适配器方法允许开发人员自定义如何调用属性的setter。
安卓属性已经创建了BindingAdapters。例如,这是PaddingLeft:
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
Binding Adapter在其他类型的自定义非常有用。例如,自定义加载器可以称为离线加载图像。
当发生冲突时,开发人员创建的绑定适配器将覆盖数据绑定的默认适配器。
你也可以利用adapter接收多个参数:
@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}"/>
在imageview使用iamgeurl这个字符串或者是error这个图片
- 命名空间在匹配期间会自动被忽略
- 你也可以为命名空间写一个adapter
Binding Adapter方法可以可选地在其处理程序中使用旧值。 使用旧值和新值的方法应该具有属性的所有旧值,然后是新值:
@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有两个方法: 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);
}
由于改变其中的一个监听器会影响另一个,所以必须创建三个不同的binding Adapter,每个属性一个外加一个为两个属性关联的:
@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作为监听器,而不是View.OnAttachStateChangeListener的set方法。 android.databinding.adapters.ListenerUtil类可以帮助跟踪以前的监听器,以便它们可以在绑定Adaper中删除。
通过使用@TargetApi(VERSION_CODES.HONEYCOMB_MR1)注释接口OnViewDetachedFromWindow和OnViewAttachedToWindow,数据绑定代码生成器知道监听器应该只在运行于Honeycomb MR1和新设备(由addOnAttachStateChangeListener(View.OnAttachStateChangeListener)支持的相同版本)上时生成。
转换器
对象转换
当一个对象从绑定表达式中返回时,一个setter方法会被选择–automatic(自动), renamed(重命名), 和custom(自定义) setters。对象会被转换为选择的setter的参数。
这对于使用ObservableMaps来保存数据的人来说很方便。 例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap返回一个在setText(CharSequence)方法中转换出来的值,当参数的类型有歧义时,开发者需要在表达式中强制转换。
自定义转换
有时候转换器应该自动转换数据类型。例如在设置背景时:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
背景应该是一个Drawable,但是color是int型。无论何时,都应该是一个drawable,int应该转换为colorDrawable。使用一个带 BindingConversion注解的静态方法可以完成这个转换:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
Note:转换器只在setter时有作用,千万不要混合使用两种类型:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
AndroidStudio支持的DataBinding
Android Studio支持许多用于数据绑定代码的代码编辑功能。例如,它支持数据绑定表达式的以下功能:
- 语法高亮显示
- 标记表达式语言语法错误
- XML代码自动补全
- 引用,包括导航(如导航到声明)和快速文档
Note:数组和泛型类型(如Observable类)可能在没有错误时显示错误。
“预览”窗格显示数据绑定表达式的默认值(如果提供)。在以下示例中,从布局XML文件中截取元素,“预览”窗格在TextView中显示PLACEHOLDER默认文本值。
<TextView android:layout_width =“wrap_content”
android:layout_height =“wrap_content”
android:text =“@ {user.firstName,default = PLACEHOLDER}”/>
如果需要在项目的设计阶段显示默认值,还可以使用工具属性而不是默认表达式值,如Designtime布局属性中所述。