引子
上图中有一些 TextView 和 Button 等,正常情况下,互联网APP都会从服务器抓取数值,然后在 Activity中 findViewById 再进行setText等等。这篇文章就是用来解放你的双手劳动力 的,使用数据绑定库可以不用去findView不用在写繁琐的 setText,只要从服务器获取json 转换成 javaBean格式然后 set,duang,,,,, 所有的值就自己展现在该有的地方了。
Demo: https://github.com/Afra55/DataBindingApplication
我自己认为,先看Demo,然后带着疑问 去阅读,会有一种解惑的情怀。>~<
原文:https://developer.android.com/intl/zh-cn/tools/data-binding/guide.html#build_environment
本文介绍了如何使用数据绑定库
数据绑定库提供了灵活性和广泛的兼容性-这是一个支持库,所以你可以在所有的android平台上使用它。这个库需要Android Plugin for Gradle 1.5.0-alpha1或更高版本。
构建环境
在 Android SDK中 下载支持库。
在 build.gradle 文件中添加 dataBinding 元素。
android {
....
dataBinding {
enabled = true
}
}
此外,确保使用的是 Android Studio,
Data Binding Layout Files
写你的第一个数据绑定表达式
数据绑定布局和普通的布局稍有不同,有一个标签为 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>
variable 的写法如下, type 的值指定完整路径的 java 类:
<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}"/>
数据对象
现在假设你有了一个 User 对象:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这种类型的对象有一个永远不会改变的数据。这是应用中是常见的,以具有被写入一次数据并随后从不改变。另外,也可以使用一个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;
}
}
从数据绑定的角度来看,这两个类是等价的。TextView
的 android:text
属性中使用的表达式 @{user.firstName}
会访问前面那个 User 类的 fristName 字段,或者后面那个 User 类的 getFirstName()
方法。或者,如果firstName()
方法存在,它也将被解析为的 firstName()
。
绑定数据
默认情况下,绑定类将基于布局文件的名称来产生,上面的布局文件的名字是 main_activity.xml
通常情况下生成的类是 MainActivityBinding
, 这个类包含所有从布局属性到布局视图的绑定 (e.g. the user variable),并且知道如何分配绑定表达式的值。下面是一个最简单的绑定的例子:
@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);
}
这样,就完成了绑定,运行程序,就可以在 ui 上看到 user 对象的数据了。另外,还可以这样获取视图:
MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
如果你正在使用数据绑定在一个ListView或RecyclerView的适配器中,最好这么用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
绑定事件
事件的绑定可能直接用来处理方法,例如 android:onClick
可以直接给 Activity 分配一个方法。事件的属性名是由监听的方法分配的,例如View.OnLongClickListener
有一个方法onLongClick()
,因此这个事件的属性是 android:onLongClick
。
分配一个事件处理机制,就是在表达式中调用这个方法名。例如,如果你的数据对象有两个方法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
public void onClickEnemy(View 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>
一些特殊的点击事件需要其他的属性来避免与 android:onClick 的冲突,已经创建了下面的属性,以避免这样的冲突:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
布局细节
Imports
import
元素允许你的布局文件引用类,就像在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}"/>
当你想引入自己写的一个 View 类的时候,你可以使用“alias”起个别名:
<import type="android.view.View"/>
<import type="com.example.real.estate.View"
alias="Vista"/>
现在,Vista 用来引用 com.example.real.estate.View,View 用来引用 android.view.View。引用类型也可能在变量的type里使用:
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List"/>
</data>
<TextView
android:text="@{((User)(userList.get(0))).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
import类型可能在要引用静态方法的时候使用:
<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, java.lang.* ,自动导入。
Variablesb(变量)
可以在 data 元素中使用任意的 variable 元素。一个 variable 元素描述了一个可能在布局文件中的绑定表达式中使用的属性。
<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>
变量的 type 是在编译时检查,因此如果一个变量实现了 Observable 或者是一个 Observable 集合,应在 type 中进行反映。如果变量是一个没有实现 Observable* 接口的基类或者接口,变量将不会被发现。
当有不同配置(如横向或纵向)的不同布局文件,变量将被合并。有这些布局文件之间一定不能有相互冲突的变量定义。
生成的绑定类会为每一个描述的变量生成一个 getter 和 setter 方法。在 setter 被调用之前,变量将采取默认的 java 值——null(引用类型),0(int),false(布尔类型)等。
一个叫 context 的特殊变量会在绑定表达式中需要的时候生成,这是 context 变量的 value 值是 Context (从根视图的 getContext 方法中得到的)。这个 context 变量会被同名的 显示声明 变量覆盖掉。
自定义绑定类的名字
默认情况下,绑定类的名字是基于布局文件的名字生成的。将第一个字母大写,去掉下划线将接下来的字母接上并大写第一个字母,最后跟上“Binding”。例如,布局文件 contact_item.xml
会生成 ContactItemBinding
,如果这个模块的包名是 com.example.my.app
,那么生成的绑定类会放置在 com.example.my.app.databinding
。
绑定类可以通过改变 data 元素下的 class
属性来重命名或者放置到其他包里。例如:
<data class="ContactItem">
...
</data>
这样绑定类就被重命名为 ContactItem
。如果绑定类要在模块包下的其他package下生成,则要加前缀 “.”
。例如:
<data class=".ContactItem">
...
</data>
这种情况下,绑定类会放到 模块包.ContactItem
下即 com.example.my.app.ContactItem
包下。如果要完全自定义包名,则提供完整路径即可:
<data class="com.example.ContactItem">
...
</data>
Includes
变量可能会被传入到引用的布局中,这么做:
<?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>
included 的布局必须要有 user 变量,例如:
<?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>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.sex}"/>
</layout>
绑定数据不支持 merge 元素的布局引用,以下布局是不支持的:
<?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表达式,下面这些都是一样的:
- 数学计算
+ - / *%
- 连接字符串
+
- 逻辑运算
&& ||
- 二进制运算
& | ^
- 单目运算
+ - ! ~
- 移位运算
>> >>> <<
- 比较运算
== > < >= <=
instanceof
- 分组
()
- 常量
character, String, numeric, null
- 造型运算符(Cast)
- 方法调用
- 字段访问
- 访问数组
[]
- 三元运算
?:
例如:
ndroid: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}"
引用属性
android:text="@{user.lastName}"
#### 避免 NullPointerException
生成的数据绑定代码会自动检查空值和避免空指针异常。例如表达式@{user.name}
,如果 user 是 null, 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="MapM<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"]}"
Resources
可以使用普通的访问资源的表达式,如下事例:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
当格式化 string 或者 plurals 需要提供参数:
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
当 plurals 有多个参数时,每个参数都要传入:
Have an orange
Have %d oranges
android:text="@{@plurals/orange(orangeCount, orangeCount)}"
下面一些资源的显示调用跟普通的调用略有不同:
Data Objects
当绑定了 java 对象后,再对他进行修改并不会让 UI 更新,这里有三个不同的数据通知机制:
当它们中的一个观察到绑定在 UI 上的数据对象被修改时,UI 就会自动更新。
Observable Objects
一个类实现 Observable
接口允许绑定对象绑定一个 listener 来监听对象所有的 属性变化。
为了让开发变得简单,有个名为 BaseObservable
的基类用来实现 listener 的注册机制,实现了这个基类的数据类会一直负责通知属性的变化,通知的方式是通过指定 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 类中生成一个属性的入口,以便在 setter 方法中通知。BR 类会在模块包下生成。
ObservableFields
Observale 类的创建会有一小部分的准备工作,因此开发者想要节约时间或者只监听少部分的属性,这时可以使用 ObservableField 和它的兄弟们 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和 ObservableParcelable。ObservableFields 是一个独立的自给自足的 ObservableFields 对象,拥有一个独立的字段。在访问原始数据时避免了 boxing 和 unboxing 操作。如下事例,在数据类中创建 public final 字段:
public class Son {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
下面是访问和设置值的例子:
son.firstName.set("Child");
int age = son.age.get();
Observable 集合
一些应用使用更多的动态结构来保存数据。Observable 集合允许使用 key-value 的形式来保存数据。 ObservableArrayMap
在当 key 是个引用类型(比如 string)的时候非常有用。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在 布局中, 使用 string key 来访问数据:
<data>
<import type="android.databinding.ObservableArrayMap"/>
<variable name="user" type="ObservableArrayMap"/>
</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
在key 是 数字的时候使用,和 ArrayList 类似:
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
在布局中,通过索引来访问数据:
<data>
<import type="android.databinding.ObservableArrayList"/>
<import type="com.example.my.app.Fields"/>
<variable name="user" type="ObservableArrayList"/>
</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"/>
生成绑定
生成的绑定类用来链接变量和布局中的视图。上文已经说过了,绑定类的名字和存储的位置都是可以自定义的。绑定类都会继承一个父类 ViewDataBinding。
创建
在布局中的视图和表达式绑定之后会很快生成绑定类。这里有几种绑定布局的方法,其中最常见的是使用绑定类中的静态方法,只需调用 inflate 方法这一步即可填充布局和绑定布局:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
如果只绑定布局,可以使用下面的方法:
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时候绑定不能被提前知道,在这种情况下可以使用 DataBindingUtil 类(该类的一些方法与文中有不同,具体看源码):
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs
布局中每一个视图的 ID 都会生成一个 public final 字段。绑定机制会在视图层次提取视图的 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>
生成的绑定类会有下面的字段:
public final TextView firstName;
public final TextView lastName;
然后可以直接使用:
mBinding.lastName.setText("Afra55");
变量
每一个变量都会有一个方法去访问它。
<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>
会在绑定类中生成相应的 setter 和 getter 方法:
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
ViewStubs 默认是不显示的,加载时才会被其他布局替换而显示出来。
ViewStub 本质上是不会出现在视图层次的, 由于 Views 是final类型,所以 ViewStub绑定后就会转换为 ViewStubProxy对象,让开发着可以在 ViewStub 存在并被填充时访问 ViewStub。
当填充另一个布局时,新布局的绑定也要被建立。因此, ViewStubProxy 一定要监听 ViewStub 的 ViewStub.OnInflateListener
接口 和及时建立绑定。因此当绑定的建立完成时 ViewStubProxy 允许开发者设置 OnInflateListener 来回调。
举例:
布局中添加 StubView:
<ViewStub
android:id="@+id/viewstub"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout="@layout/test_viewstub" />
test_viewstub.xml:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.databinding.databindingapplication.User"/>
</data>
<TextView
android:orientation="vertical"
android:text='@{user.lastName + " StubViewProxy"}'
android:gravity="center"
android:textColor="@android:color/black"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</layout>
在代码中引用:
// 设置布局加载监听用来绑定数据,绑定后 viewStub 转换成 ViewStubProxy
mBinding.viewstub.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
TestViewstubBinding viewDataBinding = DataBindingUtil.bind(inflated);
User user = new User("xx", "gg", true);
viewDataBinding.setUser(user);
}
});
// 显示布局,Andoird studio会报红,可能还么支持,直接运行即可。
if (!mBinding.viewstub.isInflated()) {
mBinding.viewstub.getViewStub().inflate();
}
Advanced Binding
动态变量
有时,一些特殊的绑定类不会被知道。例如,一个 RecyclerView.Adapter 里的操作不允许任何布局知道绑定类。但是依旧会在 onBindViewHolder(VH, int).
里分配绑定值。
在这个例子里,所有RecyclerView 布局的绑定都会有一个 “item” 变量。BindingHolder
自己实现一个 getBinding
方法用来获取 ViewDataBinding。
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
这个例子需要去实现 RecyclerView.Adapter 。我自己实现了个例子:
主布局:
<android.support.v7.widget.RecyclerView
android:id="@+id/recylerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Item 布局:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.example.databinding.databindingapplication.User" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:text="@{user.sex}" />
</FrameLayout>
</layout>
ViewHolder:
/**
* Created by yangshuai in the 21:23 of 2016.06.01 .
*/
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
private LayoutSexBinding mBinding;
public RecyclerViewHolder(View itemView) {
super(itemView);
mBinding = LayoutSexBinding.bind(itemView);
}
public ViewDataBinding getBinding() {
return mBinding;
}
}
Adapter:
/**
* Created by yangshuai in the 21:21 of 2016.06.01 .
*/
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewHolder> {
private Context mContext;
private List<User> mUsers = new ArrayList<>();
public RecyclerViewAdapter(Context context, List<User> users) {
mContext = context;
mUsers.addAll(users);
}
public void updata(List<User> users) {
mUsers.addAll(users);
notifyDataSetChanged();
}
public void clear() {
mUsers.clear();
notifyDataSetChanged();
}
@Override
public RecyclerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(mContext).inflate(R.layout.layout_sex, parent, false);
return new RecyclerViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position) {
holder.getBinding().setVariable(BR.user, mUsers.get(position));
holder.getBinding().executePendingBindings();
}
@Override
public int getItemCount() {
return mUsers.size();
}
}
代码配置:
private void initRecyclerAdapter() {
List<User> users = new ArrayList<>();
users.add(new User("unKnow"));
users.add(new User("man"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
users.add(new User("woman"));
mRecyclerViewAdapter = new RecyclerViewAdapter(this, users);
mBinding.recylerview.setLayoutManager(new LinearLayoutManager(this));
mBinding.recylerview.setItemAnimator(new DefaultItemAnimator());
mBinding.recylerview.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
mBinding.recylerview.setAdapter(mRecyclerViewAdapter);
}
效果图:
立即绑定
当绑定数据有变化时,视图的对应变化会有一段时间,所以当绑定要立刻强制执行时,使用 executePendingBindings()
方法即可。
后台线程
可以在后台线程中改变一个数据模型(非集合)。数据绑定会本地化 变量/字段 用来避免并发问题。
参数的 Setter
不论什么时候只要被绑定的值改变了,生成的绑定类都会调用 setter 方法来执行表达式并将结果展现在视图上。数据绑定框架有几种方式来自定义方法去设置值。
自动 Setter
在布局文件中与绑定表达式对应的参数,比如 TextView 的 android:text 的 值是表达式得到的结果,则如果表达式返回的是 String 类型,则回去检索 setText(String) 方法,如果表达式返回的是 int 类型,则数据绑定会去检索 setText(int) 方法。所以一定要当心表达式所返回的结果类型。可以借助这种特性,来实现自定义的 setter。比如:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}"/>
DrawerLayout 没有 scrimColor 和 drawerListener 两个参数,这两个参数与数据绑定相关联,则他们会自动去调用 方法 setScrimColor 和 setDrawerListener
方法去传递表达式的结果。
可以在自定义 view 中使用:
<com.example.databinding.databindingapplication.view.MTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@android:color/white"
app:aaaa='@{user.lastName + "AAAAA"}'
app:bbbb="@{@color/colorPrimary}"/>
app:aaaa 和 app:bbbb 是自定义的方法:
/**
* Created by yangshuai in the 20:18 of 2016.06.02 .
*/
public class MTextView extends TextView {
public MTextView(Context context) {
super(context);
}
public MTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setBbbb(int color) {
setBackgroundColor(color);
}
public void setAaaa(String text) {
setText(text);
}
}
效果如下:
重命名 setter
我们可以使用 注解 @bindingMethods 来重命名参数的 setter 方法,例如官方的这个例子 android:tint 它链接的方法是 setImageTintList(ColorStateList) 而不是 setTint:
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
不推荐这么做。
自定义 Setter
一些参数需要自定义绑定逻辑。例如,没有 setter 方法 与参数 android:paddingLeft 链接,只有 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({"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}”/>
这个绑定适配器loadImage会在 参数 imageUrl 是 string 类型和 error 是drawable 类型时被调用,并传入这两个参数的值。
绑定适配器会在 handler 持有老数据,因此方法可以 得到老数据和新数据进行比较,老数据的位置在新数据的前面:
@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());
}
}
还有事件的监听获取只允许一个 Listener 来监听:
@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);
}
}
}
当一个 接口 有多个抽象方法时,一定要把他们分离,举例:
@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 类帮助跟踪获取之前的 Listenner。
转换
Object 转换
当绑定表达式返回一个 Object 时,Object 会根据 链接的 setter 方法的输入参数来转换自己的类型。
例如:
<TextView
android:text='@{userMap["lastName"]}'
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
userMap 返回了一个 Object,这个Object 会根据检索到的方法 setText(CharSequene)自动转变类型为 CharSequene。 但有时候还需要我们自己在表达式中进行转换,看情况喽。
自定义转换
有些时候需要在特殊类型之间进行转换,例如 background,下面的写法是错误的:
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
background 参数的值应该是 Drawable,但是传入的 color 是 integer类型。 int 类型的值应该被转换成 ColorDrawable。
例如:
<com.example.databinding.databindingapplication.view.MTextView
android:layout_width="match_parent"
android:layout_height="20dp"
android:gravity="center"
android:background="@{@color/colorAccent}"
/>
普通情况下是错误的,但是在 MTextView中加上静态方法:
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
background 参数的值检查到是 int 类型时就会调用 convertColorToDrawable 方法自动转换 int 型到 ColorDrawable 从而运行成功。
下面这种情况是不允许的, 不允许混合类型:
<View
android:background="@{isError ? @drawable/error : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Android Studio 对 Data Binding 的支持
- 语法高亮
- 表达式语法错误时高亮错误
- XML 代码自动补全
- 可以通过引用快速导航
数组和泛型,就像 Observable类一样,可能在没有错误的时候显示错误,不用担心直接运行即可。
可以使用下面的方法在绑定表达式没有返回有效值时来显示默认数据:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName, default = ss}" />
这个 TextView 就会默认显示 ss 字符串。
后话
这篇文章前前后后写了有一个多月,对原文的一边翻译一边也有了自己的见解,我觉得数据绑定库是个很好的方向,希望 Google 能继续完善,比如代码的自动补全,错误提示等。
生活之于点点滴滴,积累以得源泉,祝你健康。