从我的CSDN博客http://blog.csdn.net/dahaohan 迁移的第一篇博文。
What's Data-Binding?
看过我之前转发的博文Android App的设计架构:MVC,MVP,MVVM经验谈
可以了解到移动端App开发架构从传统MVC-->MVP-->MVVM的一些进展和演化,而目前发展成的MVVM架构则需要使用Data-Binding机制来完成View和ViewModel之间的通信。
2015年google I/0开发者大会发布的Data-binding库,使得开发者可以更加简洁优雅的编写代码实现复杂的业务逻辑,而不必去关注数据变更后UI View的更新问题。View的变化会直接影响ViewModel,ViewModel的变化或者内容也会直接体现在View上,这就是Data Binding Library默默帮您完成的工作。
Android官方Data Binding介绍:https://developer.android.com/topic/libraries/data-binding/index.html
Data Binding Requirements
The Data Binding Library offers both flexibility and broad compatibility — it's a support library, so you can use it with all Android platform versions back to Android 2.1 (API level 7+).
To use data binding, Android Plugin for Gradle 1.5.0-alpha1 or higher is required.
Also, make sure you are using a compatible version of Android Studio. Android Studio 1.3 and later provides support for data binding.
Data binding 是一个类似support-v4/v7的支持库,API 7+都可以使用;
Android Studio1.3+
Android Gradle插件 1.5.0 +
How To Use Data Binding?
要使用Data binding功能需要在你app module 的build.gradle中开启。
android {
....
dataBinding {
enabled = true
}
}
<h3>Data binding Layout
使用Data binding核心是将viewModel数据嵌入到布局layout xml文件,故而layout xml文件与之前的纯view布局文件的构成略有不同。
//以下为官方给出的最简单的一个示例:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="test.example.com.databindingtest.User">
</variable>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/firstname_text"
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>
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Data binding的布局文件的根节点为layout标签,由一个数据data标签以及一个根视图标签构成。
data标签声明了该View使用的数据模型viewModel为变量名为user的User类对象,然后在Root View标签下通过
@{“变量名" + "." + ”属性名/函数名“}来给指定的view设置值。
PS:
//对于User类的另一种写法
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;
}
}
关于Data binding框架识别语句 @{user.firstName}
的一个机制是对于第一种写法访问User类的 firstName属性,而第二种写法则是访问User类的getFirstName()方法,或者也有可能访问firstName()方法如果存在的话。
<h3>Binding Data
编写完成data binding layout之后,类似IDE自动编译资源文件产生资源ID R文件,默认将以layout xml文件的名字为基础生成一个继承自ViewDataBinding的类,此例xml文件为activity_main.xml故而生成ActivityMainBinding.java
生成的ViewDataBinding类默认命名规则为,布局xml文件名去掉"_"以驼峰式写法+“Binding”,当然也支持自定义命名,后续提及
对于Android Studio IDE可以在
app/build/intermediates/classes/debug/+"对应包目录"+databinding文件夹下查看该生成的ViewDataBinding类
PS:注意这里生成的ActivityMainBinding内的以下几个属性和函数
public class ActivityMainBinding extends android.databinding.ViewDataBinding {
..........
.....
// views
//在layout xml内定义了id的 android:id="@+id/firstname_text"
//Viewdatabinding将会以驼峰命名生成对应:
//public final android.widget.TextView firstnameText;
//通过获得的binding类以及view的id名称可以直接获取该view的引用,而不需要再findViewById
public final android.widget.TextView firstnameText;
//未设置id的view则只会定义为private,外部将不可访问
private final android.widget.LinearLayout mboundView0;
private final android.widget.TextView mboundView2;
//layout内声明的变量,对应成员变量以及默认以驼峰命名生成set/get函数setUser
//用于给dataBinding设置数据viewModel
// variables
private test.example.com.databindingtest.User mUser;
........
public void setUser(test.example.com.databindingtest.User user) {
this.mUser = user;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
public test.example.com.databindingtest.User getUser() {
return mUser;
}
........
..................
}
略微查看默认生成的ActivityMainBinding类,可以看出binding类就是管理并维护View与View Model的关系的核心。包括给将user数据对应值赋值给对应的view视图;当user内值变化时更新视图;视图交互事件调用viewmodel的业务逻辑等。
ViewDataBinding相当于一个联系对应layout中View与对应ViewModel(data标签下的数据)的框架,我们使用时需要将对应的View和ViewModel实例对象传递给该ViewDataBinding框架,Data Binding库已经提供了多种方法来给实现:
@Override
protected void onCreate(Bundle savedInstanceState) {
//获取ViewDataBinding实例对象的同时已经通过传递对应的View对象或者layout文件参数,
//将对应的View实例对象传递给该ViewDataBinding框架
//Activity最常用的取代之前的setContentView方法
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
//可直接通过id名称访问对应View变量
dataBinding.firstnameText.setText("hello");
//使用默认生成的ActivityMainBinding直接bind对应View
dataBinding = ActivityMainBinding.bind(viewRoot);
//或者如下
dataBinding = ActivityMainBinding.inflate(getLayoutInflater());
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
//给DataBinding框架设置viewModel数据源User
User user = new User("Test", "User");
//set方法根据layout中声明的数据类型自动生成
binding.setUser(user);
}
上述步骤完成运行即可发现,user各属性数据自动绑定到了各自的View上,但是这只体现了dataBinding单方面从ViewModel到View的数据绑定;如何实现user数据更新然后DataBinding自动更新UiView? View的交互事件如何绑定到ViewModel的业务逻辑?
Data Binding View Event Handling##
类似data binding提供的View的数据绑定,在layout的view标签内同样允许使用表达式直接引用相应的函数处理分发的事件。使用函数引用的View 属性由对应的事件listener的函数方法决定:
//View一般有View.OnLongClickListener/ View.OnClickListener
//对应的两个方法为onLongClick/onClick故而data binding在view标签下有如下属性:
android:onClick="@{user.onLastNameClick}"
android:onLongClick="@{user.onLastNameLongClick}"
若在User类加入以下函数,则在View内完整引用监听click事件写法如下:
public class User {
....
........
public void onLastNameClick(View v){
Log.d("Test"," onLastNameClick ");
}
public boolean onLastNameLongClick(View v){
Log.d("Test"," onLastNameLongClick ");
return false;
}
....
.......
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="test.example.com.databindingtest.User">
</variable>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/firstname_text"
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}"
android:onClick="@{user.onLastNameClick}"
android:onLongClick="@{user.onLastNameClick}"/>
</LinearLayout>
</layout>
进行编译后运行即可发现点击交互事件可以顺利触发和处理。
Android官方对于View标签引用的事件处理函数的要求说明:
In your expressions, you can reference methods that conform to the signature of the listener method. When an expression evaluates to a method reference, Data Binding wraps the method reference and owner object in a listener, and sets that listener on the target view. If the expression evaluates to null, Data Binding does not create a listener and sets a null listener instead.
官方说明View处理事件引用的方法的签名要与对应的clickListener的方法签名相符,而方法的签名侧重的是方法名和方法参数的顺序、类型、个数;这里测试之后其实需要的是与对应的clickListener的方法的参数以及返回值一致。
编译期间将对View#onClick attribute的表达式引用的方法进行合法性检查,若是方法参数/返回值对应不上,则会出现编译错误:
Error:(26, 36) Listener class android.view.View.OnLongClickListener with method onLongClick did not match signature of any method user.onLastNameLongClick
若编译正确,实质其实还是将该方法包装进一个对应的listener然后给view设置对应监听接口。
Data Binding listening data/properties changes
Data binding真正的好处提现在data(user)数据变化时,能够自动更新对应的UI显示,如何使用这个核心功能呢?
<h3>Observable 对象
private static class User extends BaseObservable {
private String firstName;
private String lastName;
//Bindable注解告诉data binding框架需要侦听该值的变化
//编译期间生成的类似R文件的一个BR文件将会有该属性的一个public资源Id
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
//通过对应属性的资源ID告诉data binding框架该属性发生变化需要更新ui什么的
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
限定了修改属性数据的方法在对应的set方法内,所以在set方法更新属性后通知data binding框架属性的更新即可。
<h3>ObservableFields
其实与上述的BaseObservable对象是类似的原理,只是将整个类的范围缩小到个别需要侦听的属性上,提供了ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable.
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();
事实上每一个ObservableFields都继承自BaseObservable包含了单独一个属性值,内部默认封装了set/get方法,将上述BaseObservable的方法封装好了,也是在set方法之后通知data-binding框架做出一些更新操作。
<h3>Observable Collections
对于一些需要使用到list/map等集合数据类型来说有: ObservableArrayMap,ObservableArrayList等
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"/>
PS: Android Studio IDE目前并不能很完美的支持到data标签的一些变量的import或者是类型声明;但是并不会影响编译运行。
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("Google");
user.add("Inc.");
user.add(17);
<data>
<import type="android.databinding.ObservableList"/>
<variable name="user" type="ObservableList<Object>"/>
</data>
…
<TextView
android:text="@{user[0]}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:text="@{String.valueOf(1 + (Integer)user[2])}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Some Details
关于data标签支持import类似java的import,以及其对于一些类似map/list/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则是对应的key值
PS: Map的key为一个String时,可能遇到引号内要使用引号包括字符串的冲突,这时使用:
//单引号在外包括,内部使用双引号标示字符串
android:text='@{map["firstName"]}'
//或者外部使用双引号,内部用back quote反引号标示字符串(反引号即'~'按键)
android:text="@{map[`firstName`}"
//或者使用"即双引号的java字符实体来替代双引号
android:text="@{map["firstName"]}"
About More Details
关于data binding的细节知识推荐阅读:
官方介绍文档:https://developer.android.com/topic/libraries/data-binding/index.html
比较全面的官方文档的翻译档:http://www.jianshu.com/p/b1df61a4df77
结合实例的介绍:https://github.com/LyndonChin/MasteringAndroidDataBinding