本文为原创内容,转载请注明出处——小舍
目录
- 1 Data Binding 的结构
- 1.1 rebind 行为
- 1.2 observe data 行为
- 1.3 observe view 行为
- 2 样例分析——谷歌 sunflower 的改造
- 2.1 Data——FakeData
- 2.2 View——fragment_plant_detail.xml
- 2.3 ViewDataBinding——FragmentPlantDetailBinding
- 2.4 adapter 绑定说明
- 3 免 findViewById 的实现
1 Data Binding 的结构
作为在 Android 开发中体现 MVVM 架构思想的 Data Binding,其核心是 观察者模式 的特定实现。首先,它有三个主要的实体:
- Data:与 View 相关的数据,它可以是 View 的可观察者对象;
- View:展示给用户的视图,如果有交互功能且能更新数据,它可以是 Data 的可观察者对象;
- ViewDataBinding:连接 Data 和 View 的中介,当 Data 或 View 作为可观察者对象时,它充当可观察者对象的代理。假如当我们写了一个名为 demo.xml 的 Data Binding 的 layout 文件后,编译工具会生成一个相应的类——DemoBinding,它的原型就是 ViewDataBinding。我们通常通过
DataBindingUtil.inflate(inflater, R.layout.demo, container, false)
来实例化的 DemoBinding 对象,即 ViewDataBinding。
其次,它主要提供了以下三个方面的功能:
- 将特定的 View 与特定的 Data 进行绑定,便于模块化;
- View 自动感知和响应 Data 的变化,使得处理数据的业务层不必关心 View 的状态,便于解耦;
- Data 也可以自动同步带有交互功能的 View 对数据的修改,使得 UI 层的交互不必担心数据是否能同步 View 状态的问题,仍然便于解耦。
基于这三个功能,Data Binding 的结构也可以划分为三个行为模式,以下一一介绍:
1.1 rebind 行为
首先,Data 往往是一个数据的集合,数据绑定的第一步就是要将整个 Data 集合绑定到 View,比如初始化和数据的整体更新,如下图所示:
可以观察到,rebind 的过程就是一个简单的赋值操作,将 View 的值设置为 Data,只不过由 ViewDataBinding 这个代理来完成这个工作。图中的 _all
参数表示将 View 的所有需要更新的节点都设置为 Data 的所有对应的成员值。当我们在 layout 文件中进行如下设置时,ViewDataBinding 将代理完成 View 中所有数据绑定节点的 data rebind 操作:
<data>
<variable
name="demoData"
type="com.sample.DemoData" />
</data>
...
<!-- 将 DemoData.element1 绑定到 ChildView1 -->
<ChildView1
android:id="..."
android:width="..."
android:height="..."
app:element1="@{demoData.element1}"/>
...
<!-- 将 DemoData.element2 绑定到 ChildView2 -->
<ChildView2
android:id="..."
android:width="..."
android:height="..."
android:text="@{demoData.element2}"/>
...
1.2 observe data 行为
有时候,我们并不需要每次更新整个 Data 集合,而只需要更新集合中的某一个成员。我们希望看到的结果是,当 Data.element_i 发生变化的时候,View.child_i 更新就可以了,而不需要将 View 的所有视图节点都重新渲染一遍。要做到这一点,我们必须要让 View 可以观察 Data 的行为。换句话说,Data 是一个可观察者对象——这是 Data Binding 中另一个魅力所在,其行为模式如下:
我们可以将任何数据作为一个 Observable,然后将 ViewDataBinding 作为 View 的代理观察者,订阅 Data 的成员变化,一旦 Data 成员变化,便通知所有观察者对象——即 ViewDataBinding,然后 ViewDataBinding 再将 View 的相应节点的值设置为 Data 相应成员的新值——即图中的 _member
参数。
在 Data Binding 框架中,将 Data 设置为 Observable 的方式类似于下面的代码:
public class DemoData extends BaseObservable {
private int element;
// 定义其它成员,省略
@Bindable
public int getElement() {
return this.element;
}
public void setElement(int e) {
this.element = e;
notifyPropertyChanged(BR.element);
}
// 省略其它成员操作
}
这里有三个关键部分:
- BaseObservable:可观察者基类(实际的祖先基类是一个 Observable 接口),实现改接口后,ViewDataBinding 就会在每次 rebind 的时候去订阅 Data 的变化;
- @Bindable 标注:声明该成员是可被观察的,以及在 layout 中可以以 Data.xxx(标注的方法名如果为 getXXX)的方式进行访问;
- notifyPropertyChanged 方法:BaseObservable 用于通知具体成员发送变化的方法,只要该方法被调用,ViewDataBinding 就会检索出是哪一个 element 的变化,并只对 View 相应的节点进行更新。
1.3 observe view 行为
在开发中,根据业务需求,我们一般能遇到两种类型的 View:
- 一种是只用于展示的 View,它只展示 UI 状态,而不反馈状态,我们称之为 单工View;
- 另一种除了展示以外,还会反馈状态给监听者,我们称之为 双工View。
在 Android 的 UI事件流中,因为所有的 View 都是可以反馈状态的,所以准确来说,所有的 View 其实都是双工的。我们在这里区分单工和双工是针对业务需求的,比如:我们很多的视图只需要它们展示就可以了,不需要监听它们的状态变化,那么我们将其归为单工View。
费尽心思进行这样的划分,是因为,单工View 只需要有 observe data 行为就可以了;而双工View 往往就需要 observe view 行为。具体来说,在反馈状态时需要更新 Data 的双工 View,我们需要进行 observe view 行为。
因为双工View 会更新 Data,所以为了保证数据的一致性,Data 需要观察双工View 的状态变化。要做到这一点,这样的双工View 必须是一个可观察者对象。得益于 UI事件流的实现,双工View天然是可观察的(只要能反馈状态,就意味着能被观察)。在自定义的双工View中,可以间接引用 ViewDataBinding,这样 ViewDataBinding 就可以代理 Data 订阅 View 的状态变化:
图中我们可以看到,observe view 行为是伴随着 observe data 行为一起实现的。我们可以单独只实现 observe data 行为,但是如果要实现 observe view,必须同时实现 observe data 行为,因为该双工View 本身是也是要绑定 Data 的,也需要观察 Data 的变化——即 observe view 行为的实现需要通过双向绑定来达到,View 和 Data 同时都是对方的可观察者对象。
Data Binding 的原理基本上都在这三种行为模式的图解上,为了更深入一些实现的细节,我们下面通过一个样例来分析一下。
2 样例分析——谷歌 sunflower 的改造
sunflower app 是谷歌推出的 jetpack 库应用的最佳实践,官方只提供了 kotlin 的版本,如果大家更想看 java 版本,我改写了一个,文末放送。
我们通过 sunflower 中的一个植物种植详情页面作为具体的案例分析。为了说明 observe view 行为,我对原来的实现做了一些修改,具体不表,大家直接看现有修改后的代码分析就可以了。另外,代码也不会全贴,只贴出少量关键代码便于大家抓住重点理解。
2.1 Data——FakeData
我们定义了一个数据类——FakeData,其中有两个成员:
- viewModel:植物的详细信息,用于 data observe 行为分析;
- name: 一个测试字段,表示页面标题,用于 view observe 行为分析;
其代码如下:
public class FakeData extends BaseObservable {
private PlantDetailViewModel viewModel;
private String name;
public void setViewModel(PlantDetailViewModel viewModel) {
this.viewModel = viewModel;
// 同时通知 plant 和 isPlanted 的变化,因为这两个成员被保存在组合成员 viewModel 里
notifyPropertyChanged(BR.plant);
notifyPropertyChanged(BR.isPlanted);
}
@Bindable
public Plant getPlant() {
return viewModel != null ? viewModel.getPlant() : null;
}
@Bindable
public Boolean getIsPlanted() {
return viewModel != null ? viewModel.getIsPlanted() : false;
}
public void setName(String name) {
this.name = name;
// 通知 ViewDataBinding,name 成员变化了
notifyPropertyChanged(BR.name);
}
@Bindable
public String getName() {
return name;
}
}
这里,FakeData 暴露给外面的成员实际是三个:plant,isPlanted,name。其中 plant 和 isPlanted 由 viewModel 提供,所以当更新 viewModel 时,必须同时通知 ViewDataBinding 两个属性的更新。
2.2 View——fragment_plant_detail.xml
植物详情页的布局文件便是我们的 View 定义,我们只关注它与 FakeData 绑定的部分,且只列出部分节点:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!-- rebind -->
<variable
name="fakeData"
type="com.google.samples.apps.sunflower.data.FakeData" />
<!-- 其它Data声明 -->
......
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
...>
......
<!-- ① 绑定:observe data(赋值绑定) -->
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorSurface"
app:statusBarScrim="?attr/colorSurface"
app:collapsedTitleGravity="center"
app:collapsedTitleTextAppearance="@style/TextAppearance.Sunflower.Toolbar.Text"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:title="@{fakeData.name}"
app:titleEnabled="false