Android DataBinding
Data Binding Library 从 2015 Google I/O 上发布到至今,已经有一年多的长足发展,目前在 Android Studio2.2 版本上已经拥有比较成熟的使用体验。可以说 Data Binding 已经是一个可用度较高,也能带来实际生产力提升的技术了。
编译环境
2.0 版本以后的 Android Studio 已经内置支持了 DataBinding ,我们只要在 gradle 文件中添加如下代码就可以使用 Databinding:
android {
....
dataBinding {
enabled = true
}
}
xml 文件的处理
<layout>
<data class = "CustomBinding">
</data>
// 原来的layout
</layout>
layout
标签位于布局文件最外层,可以使原来的普通布局文件转化为 databinding layout ,同时会在build/ganerated/source/apt
下相关目录下生成 ***Binding 类
默认生成规则:xml通过文件名生成,使用下划线分割大小写,即 activity_main.xml 会生成对应的 ActivityMainBinding
data
标签用于申明 xml 文件中的变量用于绑定 View,可以通过对标签的修饰来指定生成 Binding 类的自定义名称,如上述的布局文件最终会生成一个 CustomBinding 类
Java 代码的处理
需要用 DataBindingUtil 类中的相关方法取代原先的 setContentView 及 inflate 获得 ***Binding 实例类
取代findViewById方法
findViewById(int id) 方法是将 View 的实例与 xml 布局文件中的 View 对应赋值的过程,需要遍历所有的 childrenView 查找。更关键的一点是如果比较复杂的页面,可能会存在数十个控件,光写 findViewById 也会让人挺崩溃的。虽说有着诸如 ButterKnife 这样优秀的第三方库,但使用数据绑定方式无疑更简洁明
private TextView mFirstNameTv;
private TextView mLastNameTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(this, R.layout.activity_first);
mFirstNameTv = (TextView) findViewById(R.id.tv_first_name);
mLastNameTv = (TextView) findViewById(R.id.tv_last_name);
}
//********* 或者使用 *********
private ActivityFirstBinding mFirstBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 在mBinding中有布局文件中带id的View变量
mFirstBinding = DataBindingUtil.setContentView(this, R.layout.activity_first);
}
采用 DateBinding 后,所有的 View 会在 Binding 实例类生成对应的实例,而有 id 的 View 则会使用 public 进行修饰,而变量名的生成规则是通过下划线分割大小写,即 id = "@+id/main_view"
会生成对应的 mainView 的变量,我们可以直接通过 binding.mainView
获取,直接节省了在 activity 中声明一长串变量的步骤,也不需要再写 findViewById 方法或者加上 @BindView 的注解
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_first_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
在 activity_first.xml 布局文件中添加 databindind 的 layout
标签后会生成 ActivityFirstBinding 类
// views
private final android.widget.LinearLayout mboundView0;
public final android.widget.TextView tvFirstName;
public final android.widget.TextView tvLastName;
带 id 的 view 最终会生成 public final 修饰的字段,而不带 id 的 view 也会生成 private final 修饰的字段。而这些则是在 ActivityLoginBinding 的构造函数中赋值的,仅仅只需要遍历一遍整个的 view 树,而不是多个 findViewById 方法遍历多次
为布局文件绑定Variable
数据绑定getter和setter
Variable 是 DataBinding 中的变量,可以在data
标签中添加variable
标签从而在 xml 中引入数据
<layout>
<data>
<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_first_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView
android:id="@+id/tv_last_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
variable 就是普通的 POJO 类,实现 getter 方法,并没有提供更新数据刷新 UI 的功能
private static class User {
private String firstName;
private 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;
}
}
如果希望数据变更后 UI 会即时刷新,就需要继承 Observable 类
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);
}
}
BaseObservable 提供了 notifyChange 和 notifyPropertyChanged 两个方法来刷新 UI ,前者刷新所有的值,而后者则是刷新 BR 类中有标记的属性,而 BR 类中的标记生成需要用Bindable
的标签修饰对应的 getter 方法
同时 databinding 提供了 Observable** 开头的一系列基础类可以避免继承 BaseObservable
private static class User {
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
本质上 Observable** 也是通过继承 BaseObservable 实现的,调用set方法时会调用 BaseObservable 的 notifyChange 方法
user.firstName.set("first");
String lastName = user.lastName.get();
//********************************
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
运算表达式
运算符
支持绝大部分的 Java 写法,允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式
- 算术
+
-
*
/
%
- 字符串合并
+
- 逻辑
&&
||
- 二元
&
|
^
- 一元
+
-
!
~
- 移位
>>
>>>
<<
- 比较
==
>
<
>=
<=
- instanceof
- Grouping ()
- 文字 - character, String, numeric, null
- Cast
- 方法调用
- Field 访问
- Array 访问 []
- 三目运算符
?:
尚且不支持 this,super,new 以及显式的泛型调用
空指针处理
无需判断对象是否为 null,DataBinding 会自动检查是否为 null,如果引用对象为 null,那么所有属性引用的都是 null 的,你无需判断也不会导致崩溃
空合并运算符 ??
引用的对象为 null,需要做额外的判断,DataBinding 提供了空合并运算
android:text="@{user.firstName ?? user.lastName}"
//会取第一个非空值作为结果,相当于
android:text="@{user.firstName != null ? user.firstName : user.lastName}"
集合数组的调用
对于数组,List,Map,SparseArray的访问,我们可以直接通过[]
的数组下标来访问,值得注意的是数组越界的问题
资源文件的引用
值得一说的是可以直接组合字符串
android:text="@{@string/nameFormat(firstName, lastName)}"
<string name="nameFormat">%s, %s</string>
也可以对数值类应用直接进行运算
android:marginLeft="@{@dimen/margin + @dimen/avatar_size}"
需要注意的是一些资源文件需要确切的名称
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
属性关联
DataBinding 库通过解析 View 的 setter 方法来完成赋值过程,android:text = "@user.firstName"
就相关于调用了
TextView 的 tv.setText(user.firstName)
甚至可以调用 View 未提供的布局属性,只要 View 提供了对应的 setter 方法。
举个例子:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"/>
DrawerLayout 有个 setScrimColor(int color)
方法,所以可以在布局中使用未定义的app:scrimColor
属性,通过 app 命名空间修饰的属性会自动关联到对应的方法
属性扩展
BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局属性并没有完整对应的方法提供,比如说 ImageView 的"android:tint"
布局属性的对应方法是setImageTintList(@Nullable ColorStateList tint)
,这时就需要使用 DataBinding 提供的处理方法,使用BindingMethods
注解
@BindingMethods({
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, String imageUri) {
if (imageUri == null) {
view.setImageURI(null);
} else {
view.setImageURI(Uri.parse(imageUri));
}
}
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, Uri imageUri) {
view.setImageURI(imageUri);
}
@BindingAdapter("android:src")
public static void setImageDrawable(ImageView view, Drawable drawable) {
view.setImageDrawable(drawable);
}
}
这是系统提供的 ImageViewBindingAdapter,可以在引入了 DataBinding 后全局搜索查看详情,通过BindingMethod
注解将两者关联起来,但是如果 View 甚至没有实现对应方法或者需要绑定自定义方法,这是可以使用BindingAdapter
注解
BindingConversion 注解
有时在 xml 中绑定的属性,未必是最后的set方法需要的,比如想用color(int),但是 view 需要 Drawable,比如我们想用String,而 view 需要的是 Url 。这时候就可以使用BindingConversion
注解
<View
android:background=“@{isError ? @color/red : @color/white}”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”/>
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
链式表达式
<ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<CheckBox android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
代码可以优化成
<ImageView android:id=“@+id/avatar”
android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/>
<TextView android:visibility=“@{avatar.visibility}”/>
<CheckBox android:visibility="@{avatar.visibility}"/>
在系统生成的 Bindinng 类中,会被解析成这三个控件可见性都跟随着 user.isAdult 的状态而改变
使用Callback
事件绑定
DataBinding 不仅可以在布局文件中为控件绑定数值,也可以在布局文件中为控件绑定监听事件
- android:onClick
- android:onLongClick
- android:onTouch
- ......
通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View"/>
<variable
name="user"
type="com.sanousun.sh.databinding.bean.User"/>
<variable
name="presenter"
type="com.sanousun.sh.databinding.activity.SecondActivity.Presenter"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_mobile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{presenter::mobileClick}"
android:text="@{user.firstName}"/>
<TextView
android:id="@+id/tv_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->presenter.pwdClick()}"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
在java代码中:
public class Presenter {
public void mobileClick(View view) {
Toast.makeText(SecondActivity.this, "mobile click", Toast.LENGTH_LONG).show();
}
public void pwdClick() {
Toast.makeText(SecondActivity.this, "pwd click", Toast.LENGTH_LONG).show();
}
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSecondBinding = DataBindingUtil.setContentView(this, R.layout.activity_second);
mSecondBinding.setUser(new User("da", "shu"));
mSecondBinding.setPresenter(new Presenter());
}
事件绑定使用 lambda 表达式,绑定形式主要是有两种形式:
Method References
需要方法参数及返回值与对应的 listener 一致,在编译时生成对应的 listenerImpl 并在放置 presenter 时为对应控件添加监听,如上面的 mobileClick
// Listener Stub Implementations
public static class OnClickListenerImpl implements android.view.View.OnClickListener{
private com.sanousun.sh.databinding.activity.SecondActivity.Presenter value;
public OnClickListenerImpl setValue(com.sanousun.sh.databinding.activity.SecondActivity.Presenter value) {
this.value = value;
return value == null ? null : this;
}
@Override
public void onClick(android.view.View arg0) {
this.value.mobileClick(arg0);
}
}
代码中会做 presenter 的空判断
Listener Bindings
无需匹配对应 listener 的参数,只需要保证返回值的一致即可(除非是void)。与 Method References 的最大的不同点在于
它是在点击事件发生时相应的
// callback impls
public final void _internalCallbackOnClick(int sourceId , android.view.View callbackArg_0) {
// localize variables for thread safety
// presenter
com.sanousun.sh.databinding.activity.SecondActivity.Presenter presenter = mPresenter;
// presenter != null
boolean presenterObjectnull = false;
presenterObjectnull = (presenter) != (null);
if (presenterObjectnull) {
presenter.pwdClick();
}
}
这个方法会在页面有点击时间时调用,同样也会做空判断
当然你也可以通过@BindingMethods
和@BindingAdapter
进行自定义的扩展
双向绑定
有别于单向绑定使用的@{}
符号,双向绑定使用@={}
符号用于区别,目前支持的属性有 text,checked,year,month,day,hour,rating,progress 等
InverseBindingListener
实现双向绑定需要归功于 DataBinding 库中的 InverseBindingListener 接口,这个监听器的作用是监听目标控件的属性改变
private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of user.male
// is user.setMale((boolean) callbackArg_0)
boolean callbackArg_0 = mboundView1.isChecked();
// localize variables for thread safety
// user.male
boolean maleUser = false;
// user
com.sanousun.sh.databinding.bean.User user = mUser;
// user != null
boolean userObjectnull = false;
userObjectnull = (user) != (null);
if (userObjectnull) {
user.setMale((boolean) (callbackArg_0));
}
}
};
对应 DataBinding 类中有根据双向绑定生成的 Inverse Binding Event Handlers
@Override
protected void executeBindings() {
......
android.databinding.adapters.CompoundButtonBindingAdapter.setListeners(this.mboundView1, (android.widget.CompoundButton.OnCheckedChangeListener)null, mboundView1androidCh);
}
在绑定时,设置到对应的控件中,当监听控件属性改变时,就会触发重绑定,更新属性值
InverseBindingMethods 和 InverseBindingAdapter 注解
如果你想做自定义的双向绑定,你必须充分理解这几个注解的含义。
@Target({ElementType.ANNOTATION_TYPE})
public @interface InverseBindingMethod {
Class type();
String attribute();
String event() default ""; // 默认会根据attribute name获取get
String method() default "";// 默认根据attribute增加AttrChanged
}
以系统定义的 CompoundButtonBindingAdapter 为例
@BindingMethods({
@BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"),
@BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"),
})
@InverseBindingMethods({
@InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),
})
public class CompoundButtonBindingAdapter {
@BindingAdapter("android:checked")
public static void setChecked(CompoundButton view, boolean checked) {
if (view.isChecked() != checked) {
view.setChecked(checked);
}
}
@BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"},
requireAll = false)
public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener,
final InverseBindingListener attrChange) {
if (attrChange == null) {
view.setOnCheckedChangeListener(listener);
} else {
view.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (listener != null) {
listener.onCheckedChanged(buttonView, isChecked);
}
attrChange.onChange();
}
});
}
}
}
双向绑定需要为属性绑定一个监听器,这里就是需要为"android:checked"
属性绑定监听器,通过 @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"),databinding 可以通过 checkedAttrChanged 找到 OnCheckedChangeListener,设置 OnCheckedChangeListener 来通知系统生成的 InverseBindingListener 调用 onChange 方法,从而通过 getter 方法来获取值。值得注意的是为了防止无限循环调用,setter 方法必须要去进行重判断
同样如果没有对应方法,可以自定义 InverseBindingAdapter 来实现,详情见系统TextViewBindingAdapter
隐式调用
实现了双向绑定的属性就可以隐式调用,而不用写繁琐的 listener
<CheckBox android:id="@+id/cb"/>
<ImageView android:visibility="@{cb.checked ? View.VISIBLE : View.GONE}"/>
属性改变监听
当然我们可以通过 Observable.OnPropertyChangedCallback 来监听属性的改变,从而实现具体的业务逻辑
user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
if (i== BR.firstName){
Toast.makeText(ThirdActivity.this, user.getFirstName(), Toast.LENGTH_LONG).show();
}
}
});
RecyclerView的处理
只要简单的定义 ViewHolder
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected final T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding() {
return mBinding;
}
}
因为逻辑和属性的绑定在xml中就已经处理好,adapter 的创建变得十分的容易,一般情况下可以直接使用,如果需要额外的更改可以继承。而点击事件的监听可以在 onBindViewHolder 中设置
对于含有多种 viewType 的列表适配器,在不同 xml 布局文件中 variable 的 name 可以全部写为 item,那么在绑定数据时
无需特殊处理
@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
final Data data = mData.get(position);
holder.getBinding().setVariable(item, data);
holder.getBinding().executePendingBindings();
}
在生成的代码中会去检查它的类型,并将其赋值
高级用法
component 注入
Data Binding Component详解 - 换肤什么的只是它的一个小应用!
原理简述
解析
编译时,系统会将 xml 文件拆分为两部分,数据部分的 xml 和布局部分的 xml,分别存放于app/build/intermediates/data-binding-info
和app/build/intermediates/data-binding-layout-out
之中,数据部分的 xml 文件记录 view 对应的赋值表达式,而布局部分的 xml 则是普通的布局如下
<Button
android:id="@+id/btn_btn"
android:layout_width="match_parent"
android:layout_height="56dp"
android:tag="binding_1"/>
特殊在于每个控件都会生成 tag,作用是生成 DataBinding 时可以绑定对应控件,因此在布局文件中需要避免书写tag
解析xml -> 解析表达式 -> java编译 —> 解析依赖 -> setter
public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) {
super(bindingComponent, root, 1);
final Object[] bindings = mapBindings(bindingComponent, root, 4, sIncludes, sViewsWithIds);
this.activityMain = (android.widget.LinearLayout) bindings[0];
this.activityMain.setTag(null);
this.btnBtn = (android.widget.Button) bindings[1];
this.btnBtn.setTag(null);
setRootTag(root);
// listeners
invalidateAll();
}
在生成的 binding 类中,构造函数会为所有的控件赋值,此时会将 tag 值去除,所以说为 View 的赋值需要在获取 DataBinding 实例之后。初始化时遍历 view 赋值比 findViewById 效率高得多
绑定
绑定的代码都在生成的 DataBinding 类中的 executeBindings 方法中,不管任何涉及到更新 ui 的地方最终都会调用这个方法
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
//一些变量的定义
......
if ((dirtyFlags & 0x5L) != 0) {
//根据flag的值判断是否需要做相应的改变
......
}
......
}
databinding 使用位标记来检验更新(dirtyFlags),每一个标志位都有自己的含义,生成的规则由内部解析表达式后确定,在ViewDataBinding 中我们可以看到
if (USE_CHOREOGRAPHER) {
mChoreographer = Choreographer.getInstance();
mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mRebindRunnable.run();
}
};
} else {
mFrameCallback = null;
mUIThreadHandler = new Handler(Looper.myLooper());
}
批量刷新会发生在系统的帧布局刷新时,系统帧布局刷新回调 -> mRebindRunnable -> executePendingBindings -> executeBindings,此时才会触发数据更改的操作
更新
刷新布局最终都会调用 executeBindings 方法,而在父类 ViewDataBinding 类是由 executePendingBindings 调用方法,我们可以直接调用此方法来加载挂起的属性变更,而不用等待下一次的帧布局刷新
而所有的 Variable 内部属性的改变则会注册监听器,监听改变 -> handleFieldChange -> requestRebind -> executePendingBindings -> executeBindings 最终改变属性
参考
从零开始的Android新项目7 - Data Binding入门篇
棉花糖给 Android 带来的 Data Bindings(数据绑定库)