862fd8f25b558d3273f358e6301f3d69
重学安卓:从 被误解 到 真香 的 Jetpack DataBinding!

公告:我们最近在 GitHub 发起了一项《Jetpack 传道者》计划,专注于提供 Jetpack 相关组件的 跨语言对照示例、自助服务、以及 Motion 动画作品征集,感兴趣的小伙伴可前往查阅或根据活动方式参与 😉

https://github.com/Jetpack-Missionary

前言

很高兴见到你!

前面几期,我们在深度思考的帮助下,通过分析 背景状况身世使命,分别掌握了 Lifecycle、LiveData、ViewModel 的存在缘由、职责边界、工作目标。

相信阅读完这 3 篇的小伙伴,对 视图控制 之 状态管理标准化开发模式 的印象,已跑赢 90% 的 Andorid 开发者。😉

毫不夸张地说,在标准化开发模式的加持下,软件开发效率会 因为不可预期的错误被减少 90%,而超大幅度地提升。

而且只要确立了这样的开发模式,往后软件开发便可以不假思索地按部就班,省去了大量因模棱两可、犹豫不决而耽误的时间!

被不假思索地抵制的 DataBinding

原本 视图控制 的标准开发模式,到此为止就要宣告完结,

没想到在和一位 “曾任腾讯 QQ 8 年工龄核心员工 的 现任深圳南山某自动化代码生产工具” CEO 的技术攀谈中,在他再三推荐 DataBinding 后,我对 DataBinding 重新报以重视,

并且在真正地经过深度思考以后,才发现,DataBinding 不是我和许多人原本想当然以为的那样。

相信许多人对 DataBinding 的印象仅仅停留在:

一看 xml 中居然还要写 三元表达式 “这样的逻辑”,便草草地断定:DataBinding 不是个好东西,在声明式编程中书写 UI 逻辑,既不可调试,也不便于察觉和追踪,万一出现问题就麻烦了。

事实果真是这样吗?

文章目录一览

  • 前言
  • 被不假思索地抵制的 DataBinding
  • DataBinding 的目标只有三个
  • DataBinding 问世前的混沌世界
  • DataBinding 为什么能解决这三个问题?
  • 造成人们误认为 DataBinding 是在 xml 中写 UI 逻辑的原因
  • DataBinding 结合 LiveData 的使用
  • Note 2019.12.12 加餐
    • LiveData 和 ObservableField 的本质区别
  • BindingAdapter 在解决 Drawable 复用问题上的绝妙使用
  • Note 2020.4.26 加餐
    • DataBinding 参数语法为何如此设计
  • Note 2020.5.6 加餐
    • BindingMethod 与 BindingAdapter 的本质区别
  • Note 2020.4.20 加餐
    • ViewBinding 和 DataBinding 不具可比性
  • DataBinding 的注意事项 和 屡试不爽的排错技巧
  • 综上

DataBinding 的目标只有三个

同此前的所有文章一样,我写作的目的有且只有一个:帮助读者快速明确状况 —— 究竟是什么原因,导致某项技术的存在、某项技术究竟是为解决什么问题而存在。

所以本文也是一样开门见山 —— DataBinding 的存在,是为了实现唯一的核心目标:

通过 基于适配器模式 的 “数据驱动” 思想,规避视图调用的一致性问题

并且数据驱动 顺带免去了 因调用视图对象 而存在的大量冗余的判空处理。

并且数据驱动 顺带免去了 为调用视图对象 而存在的大量样板代码。

如果光是阅读了以上几点,你还是不太理解的话,那接下来我就来介绍 99% 的网文都不曾为你明确的真实状况。😉

DataBinding 问世前的混沌世界

例如 3 年前我 自主设计并开发的一款日记软件,该软件包含横、竖屏布局。

该 calendar 日历控件只存在于横屏布局中。

因此如果是以往的开发模式,我们需要在每个 Fragment 刷新 calendar 状态的地方,去逐个调用 calendar 控件,手动 set 新的状态,并且 set 前还要判断该控件存不存在。

那么这就造成一致性的问题:因为人类不是机器,人类一定会有疏忽的时候。特别是项目中包含数十个横竖屏不同布局方案、而每个页面又在多个方法中调用多个控件的情况,当后期项目改动,或换个同事接手时,很容易因疏忽了某些控件的存在与否,而埋下 “空指针异常” 的隐患。

同理,为了拿到控件最终的状态,我们仍然是通过 调用控件的方式去获取状态,如此,若该控件不存在,可能造成空指针异常;而如果大量判空,代码就不优雅、十分冗余。

除了一致性问题,还有个很明显的问题,就是每新建一个页面,都需要为各种控件 findViewById —— 这些令人难以忍受的样板代码。

何况我从不使用 ButterKnife,因为它仅仅是为了解决样板代码,并且还需要额外地在代码中书写注解,看上去十分蹩脚。

那怎么办呢?

DataBinding 为什么能解决这三个问题?

首先,数据驱动 意味着,控件的状态被分离到 ViewModel 中管理,并且 ViewModel 这一层只需负责状态变量本身的变化,至于该变量在布局中究竟被哪些视图绑定、当前有没有视图来绑定,ViewModel 不用管。

那控件是如何做到被通知且更新状态的呢?

DataBinding 是通过 适配器模式和观察者模式 来管理控件刷新状态。

当状态变量发生变化时,需要开发者手动地完成状态的更新。这将通知 DataBinding 中绑定该变量的控件刷新数据。

在 BaseObservable 子类中,为 getter 赋予了 @Bindable 注解的,在编译后会生成与之相关的 BR 变量,于是 通过在 setter 中调用 notifyPropertyChanged 方法,可以通知 DataBinding 去刷新绑定该状态的控件

具体的代码可参考:在 Android 项目结构树状图中,通过 Java(Generate)节点 查看 目录下生成的 DataBindingImpl 文件(例如《重学安卓》项目生成的 FragmentTestDatabindingObserveBindingImpl),其中 executeBindings 方法,就是数据刷新的逻辑所在。

executeBindings 会依据 @Bindable 生成的 dirtyFlags 来区分当前是哪个 BR 被通知变化,从而只更新与之相关的控件,这么设计的缘由很容易理解:

1.为了 “精准投放”,而不是所有控件都重绘,从而维持良好的性能。

2.当多个控件绑定了同一个状态变量时,这些控件因为 dirtyFlags 一致的缘故,都会被通知,而无需开发者手动在 Java 代码中逐个调用控件去刷新。

但 Observable 的做法 反而滋生了另一种一致性问题:

例如状态变量对应的类中,某些成员要是遗漏了 notifyPropertyChanged,这个成员的通知刷新就会不起效。

所以说,数据驱动思想是好的,实现了视图与状态的分离。就是实现上有点不尽人意,居然需要开发者手动去通知。

那怎么办呢?于是 Google 又设计了一种 更为简便的状态声明方式

通过 ObservableField。

于是你只需在 ViewModel 中 分别声明该页面所包含的状态,然后在布局中与控件一对一地绑定就 OK 了。

如图,每个 ObservableField 都是持有 基本数据类型 或 String,来 作为单一的 State 去绑定 布局中对应控件的属性,从而当从外界为 ObservableField set 新的值、也即 state 发生变化时,能精准地只通知绑定了的控件发生重绘

再就是双向绑定,双向绑定的存在 主要是为了 避免直接调用控件示例来获取数据,来避免视图调用的一致性问题。

比如登录的场景,用户名、密码输入完毕点击 “提交”,那么在双向绑定的帮助下,我们只需通过 State-ViewModel 拿到与 用户名、密码 输入框发生双向绑定的 ObservableField 即可。

双向绑定的书写方式是在普通绑定的基础上加个等号,例如:

android:text="@={vm.name}"

所以,目前为止,相信大家在 Fragment 等视图控制器中对控件视图的判空,已经趋近于 0 了,因为数据驱动,完全不需要手动调用视图。

所以光是数据驱动,就已经解决了 findViewById 的问题、并且一口气把最开始提到的 3 个目标全解决了。😃

人们误认为 DataBinding 是在 xml 中写 UI 逻辑的原因

回到最初,文章开头提到的那种写法,是 官方文档 给出的具有误导性的 糟糕示例。

top Created with Sketch.