Data Binding Library 从 2015 Google I/O 上发布到至今,已经有一年多的长足发展,目前在 Android Studio2.2 版本上已经拥有比较成熟的使用体验。可以说 Data Binding 已经是一个可用度较高,也能带来实际生产力提升的技术了。
编译环境
2.0 版本以后的 Android Studio 已经内置支持了 DataBinding ,我们只要在 gradle 文件中添加如下代码就可以使用 Databinding:
1 | android { |
xml 文件的处理
1 | <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 这样优秀的第三方库,但使用数据绑定方式无疑更简洁明
1 | private TextView mFirstNameTv; |
采用 DateBinding 后,所有的 View 会在 Binding 实例类生成对应的实例,而有 id 的 View 则会使用 public 进行修饰,而变量名的生成规则是通过下划线分割大小写,即 id = "@+id/main_view"
会生成对应的 mainView 的变量,我们可以直接通过 binding.mainView
获取,直接节省了在 activity 中声明一长串变量的步骤,也不需要再写 findViewById 方法或者加上 @BindView 的注解
1 | <layout |
在 activity_first.xml 布局文件中添加 databindind 的 layout
标签后会生成 ActivityFirstBinding 类
1 | // views |
带 id 的 view 最终会生成 public final 修饰的字段,而不带 id 的 view 也会生成 private final 修饰的字段。而这些则是在 ActivityLoginBinding 的构造函数中赋值的,仅仅只需要遍历一遍整个的 view 树,而不是多个 findViewById 方法遍历多次
为布局文件绑定Variable
数据绑定getter和setter
Variable 是 DataBinding 中的变量,可以在data
标签中添加variable
标签从而在 xml 中引入数据
1 | <layout> |
variable 就是普通的 POJO 类,实现 getter 方法,并没有提供更新数据刷新 UI 的功能
1 | private static class User { |
如果希望数据变更后 UI 会即时刷新,就需要继承 Observable 类
1 | private static class User extends BaseObservable { |
BaseObservable 提供了 notifyChange 和 notifyPropertyChanged 两个方法来刷新 UI ,前者刷新所有的值,而后者则是刷新 BR 类中有标记的属性,而 BR 类中的标记生成需要用Bindable
的标签修饰对应的 getter 方法
同时 databinding 提供了 Observable** 开头的一系列基础类可以避免继承 BaseObservable
1 | private static class User { |
本质上 Observable** 也是通过继承 BaseObservable 实现的,调用set方法时会调用 BaseObservable 的 notifyChange 方法
1 | user.firstName.set("first"); |
运算表达式
运算符
支持绝大部分的 Java 写法,允许变量数据访问、方法调用、参数传递、比较、通过索引访问数组,甚至还支持三目运算表达式
- 算术
+
-
*
/
%
- 字符串合并
+
- 逻辑
&&
||
- 二元
&
|
^
- 一元
+
-
!
~
- 移位
>>
>>>
<<
- 比较
==
>
<
>=
<=
- instanceof
- Grouping ()
- 文字 - character, String, numeric, null
- Cast
- 方法调用
- Field 访问
- Array 访问 []
- 三目运算符
?:
尚且不支持 this,super,new 以及显式的泛型调用
空指针处理
无需判断对象是否为 null,DataBinding 会自动检查是否为 null,如果引用对象为 null,那么所有属性引用的都是 null 的,你无需判断也不会导致崩溃
空合并运算符 ??
引用的对象为 null,需要做额外的判断,DataBinding 提供了空合并运算
1 | android:text="@{user.firstName ?? user.lastName}" |
集合数组的调用
对于数组,List,Map,SparseArray的访问,我们可以直接通过[]
的数组下标来访问,值得注意的是数组越界的问题
资源文件的引用
值得一说的是可以直接组合字符串
1 | android:text="@{@string/nameFormat(firstName, lastName)}" |
也可以对数值类应用直接进行运算
1 | 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 方法。
举个例子:
1 | <android.support.v4.widget.DrawerLayout |
DrawerLayout 有个 setScrimColor(int color)
方法,所以可以在布局中使用未定义的app:scrimColor
属性,通过 app 命名空间修饰的属性会自动关联到对应的方法
属性扩展
BindingMethods 和 BindingAdapter 注解
但是部分 View 的布局属性并没有完整对应的方法提供,比如说 ImageView 的"android:tint"
布局属性的对应方法是setImageTintList(@Nullable ColorStateList tint)
,这时就需要使用 DataBinding 提供的处理方法,使用BindingMethods
注解
1 | @BindingMethods({ |
这是系统提供的 ImageViewBindingAdapter,可以在引入了 DataBinding 后全局搜索查看详情,通过BindingMethod
注解将两者关联起来,但是如果 View 甚至没有实现对应方法或者需要绑定自定义方法,这是可以使用BindingAdapter
注解
BindingConversion 注解
有时在 xml 中绑定的属性,未必是最后的set方法需要的,比如想用color(int),但是 view 需要 Drawable,比如我们想用String,而 view 需要的是 Url 。这时候就可以使用BindingConversion
注解
1 | <View |
1 |
|
链式表达式
1 | <ImageView android:visibility=“@{user.isAdult ? View.VISIBLE : View.GONE}”/> |
代码可以优化成
1 | <ImageView android:id=“@+id/avatar” |
在系统生成的 Bindinng 类中,会被解析成这三个控件可见性都跟随着 user.isAdult 的状态而改变
使用Callback
事件绑定
DataBinding 不仅可以在布局文件中为控件绑定数值,也可以在布局文件中为控件绑定监听事件
- android:onClick
- android:onLongClick
- android:onTouch
- ……
通常会在java代码中定义一个名为Handler或者Presenter的类,然后set进来
1 | <layout |
在java代码中:
1 | public class Presenter { |
事件绑定使用 lambda 表达式,绑定形式主要是有两种形式:
Method References
需要方法参数及返回值与对应的 listener 一致,在编译时生成对应的 listenerImpl 并在放置 presenter 时为对应控件添加监听,如上面的 mobileClick
1 | // Listener Stub Implementations |
代码中会做 presenter 的空判断
Listener Bindings
无需匹配对应 listener 的参数,只需要保证返回值的一致即可(除非是void)。与 Method References 的最大的不同点在于
它是在点击事件发生时相应的
1 | // callback impls |
这个方法会在页面有点击时间时调用,同样也会做空判断
当然你也可以通过@BindingMethods
和@BindingAdapter
进行自定义的扩展
双向绑定
有别于单向绑定使用的@{}
符号,双向绑定使用@={}
符号用于区别,目前支持的属性有 text,checked,year,month,day,hour,rating,progress 等
InverseBindingListener
实现双向绑定需要归功于 DataBinding 库中的 InverseBindingListener 接口,这个监听器的作用是监听目标控件的属性改变
1 | private android.databinding.InverseBindingListener mboundView1androidCh = new android.databinding.InverseBindingListener() { |
对应 DataBinding 类中有根据双向绑定生成的 Inverse Binding Event Handlers
1 | @Override |
在绑定时,设置到对应的控件中,当监听控件属性改变时,就会触发重绑定,更新属性值
InverseBindingMethods 和 InverseBindingAdapter 注解
如果你想做自定义的双向绑定,你必须充分理解这几个注解的含义。
1 | @Target({ElementType.ANNOTATION_TYPE}) |
以系统定义的 CompoundButtonBindingAdapter 为例
1 | ({ |
双向绑定需要为属性绑定一个监听器,这里就是需要为"android:checked"
属性绑定监听器,通过 @InverseBindingMethod(type = CompoundButton.class, attribute = “android:checked”),databinding 可以通过 checkedAttrChanged 找到 OnCheckedChangeListener,设置 OnCheckedChangeListener 来通知系统生成的 InverseBindingListener 调用 onChange 方法,从而通过 getter 方法来获取值。值得注意的是为了防止无限循环调用,setter 方法必须要去进行重判断
同样如果没有对应方法,可以自定义 InverseBindingAdapter 来实现,详情见系统TextViewBindingAdapter
隐式调用
实现了双向绑定的属性就可以隐式调用,而不用写繁琐的 listener
1 | <CheckBox android:id="@+id/cb"/> |
属性改变监听
当然我们可以通过 Observable.OnPropertyChangedCallback 来监听属性的改变,从而实现具体的业务逻辑
1 | user.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() { |
RecyclerView的处理
只要简单的定义 ViewHolder
1 | public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder { |
因为逻辑和属性的绑定在xml中就已经处理好,adapter 的创建变得十分的容易,一般情况下可以直接使用,如果需要额外的更改可以继承。而点击事件的监听可以在 onBindViewHolder 中设置
对于含有多种 viewType 的列表适配器,在不同 xml 布局文件中 variable 的 name 可以全部写为 item,那么在绑定数据时
无需特殊处理
1 | @Override |
在生成的代码中会去检查它的类型,并将其赋值
高级用法
component 注入
Data Binding Component详解 - 换肤什么的只是它的一个小应用!
原理简述
解析
编译时,系统会将 xml 文件拆分为两部分,数据部分的 xml 和布局部分的 xml,分别存放于app/build/intermediates/data-binding-info
和app/build/intermediates/data-binding-layout-out
之中,数据部分的 xml 文件记录 view 对应的赋值表达式,而布局部分的 xml 则是普通的布局如下
1 | <Button |
特殊在于每个控件都会生成 tag,作用是生成 DataBinding 时可以绑定对应控件,因此在布局文件中需要避免书写tag
解析xml -> 解析表达式 -> java编译 —> 解析依赖 -> setter
1 | public ActivityMainBinding(android.databinding.DataBindingComponent bindingComponent, View root) { |
在生成的 binding 类中,构造函数会为所有的控件赋值,此时会将 tag 值去除,所以说为 View 的赋值需要在获取 DataBinding 实例之后。初始化时遍历 view 赋值比 findViewById 效率高得多
绑定
绑定的代码都在生成的 DataBinding 类中的 executeBindings 方法中,不管任何涉及到更新 ui 的地方最终都会调用这个方法
1 | @Override |
databinding 使用位标记来检验更新(dirtyFlags),每一个标志位都有自己的含义,生成的规则由内部解析表达式后确定,在ViewDataBinding 中我们可以看到
1 | if (USE_CHOREOGRAPHER) { |
批量刷新会发生在系统的帧布局刷新时,系统帧布局刷新回调 -> mRebindRunnable -> executePendingBindings -> executeBindings,此时才会触发数据更改的操作
更新
刷新布局最终都会调用 executeBindings 方法,而在父类 ViewDataBinding 类是由 executePendingBindings 调用方法,我们可以直接调用此方法来加载挂起的属性变更,而不用等待下一次的帧布局刷新
而所有的 Variable 内部属性的改变则会注册监听器,监听改变 -> handleFieldChange -> requestRebind -> executePendingBindings -> executeBindings 最终改变属性
参考
从零开始的Android新项目7 - Data Binding入门篇
棉花糖给 Android 带来的 Data Bindings(数据绑定库)