耳目一新 Jetpack MVVM 精讲
2023-08-05 14:59:53

前言

最近后台时而收到读者留言,说能否出一期 Jetpack MVVM 精讲,以及配套一份简练案例,好把玩把玩、感受感受、加深 MVVM 印象。

答案是肯定的。

面向标准化开发已成现实

金九银十,相信不少读者在抓紧机会面试。

Android 市场已今非昔比。过去迫于招人压力,应试者只需了解四大组件、视图、网络请求,即可谋得一份满意工作。

现如今,Jetpack 架构组件及 “标准化开发模式” 确立,预示 Android 开发已步入成熟阶段:

许多 “样板代码” 不再需开发者手写,而是可通过 “模版工具” 自动生成,在取缔繁杂耗时重复工作同时,避免因 “人工操作疏忽” 造成难以排查、不可预期错误

这十分符合企业利益,因而面试官招人时,也更加看重应试者对架构组件 —— 至少是 MVVM 理解程度。

像 “解耦” 等含糊其辞说法,不再为面试官认可,稍有 MVVM 经验面试官,都会请你举例说明,好证实你对 MVVM 确有深入理解,能自然而然写出标准化、规范化代码,能迅速适应各公司自制 “自动化模版工具”。

本文目标

本人拥有 3 年 “移动端架构” 践行和设计经验,领导或参与团队 “重构” 中大型项目多达十数个,对 Jetpack MVVM 架构在 “确立规范化、标准化开发模式,以减少不可预期错误” 所作努力,有深入理解。

因而本文目标,就是结合前几期我们分别 “深入浅出” 介绍的 Lifecycle、LiveData、ViewModel、DataBinding 来融汇贯通演绎一下:

作为 “应用开发骨架” 的 “标准化状态管理框架”,究竟为快速开发过程中 “减少不可预期错误” 做了哪些努力。

不同于 “东拼西凑、人云亦云、徒添困扰” 网文,愿意将标准化开发模式 深度思考知识实战反思经验 无保留分享,全网仅此一家。这样文章可以说 看一篇、少一篇,因此,就算不去 hold 住面试官,也请务必跟随本文脚步,将 Jetpack MVVM 来龙去脉过一遍。

文章目录一览

  • 前言
  • 面向标准化开发已成现实
  • 本文目标
  • Jetpack Lifecycle
    • Lifecycle 存在前的混沌世界
    • Lifecycle 为什么能解决上述这些问题?
  • Jetpack LiveData
    • LiveData 存在前的混沌世界
    • LiveData 为什么能解决上述这些问题?
    • LiveData 有个坑需要注意
  • Jetpack ViewModel
    • ViewModel 存在前的混沌世界
    • ViewModel 为什么能做到这几点?
  • Jetpack DataBinding
    • DataBinding 存在前的混沌世界
    • DataBinding 就是来解决这些问题
  • 综上

Jetpack Lifecycle

Lifecycle 的存在,主要为了解决 “生命周期管理” 一致性问题

Lifecycle 存在前的混沌世界

Lifecycle 面市前,“生命周期管理” 纯靠手工维持,这容易滋生大量一致性问题。

例如跨页面共享 GpsManager 组件,在每个宿主 Activity 的 onResume 和 onPause 中都需 手动 “激活、解绑 和 叫停”

那么 随着宿主 Activity 增多,这种手动操作埋下的 “一致性隐患” 就会指数级增长

一方面,凡需手工维持,开发者皆易疏忽,特别是工作交接给其他同事时,同事并不能及时注意到这些细节。

另一方面,分散的代码不利于修改,日后除了激活、叫停,若有其他操作补充,则每个宿主 Activity 都需额外书写一遍。

Lifecycle 为何能解决上述这些问题?

Lifecycle 通过 “模板方法模式” 和 “观察者模式”,将生命周期管理的复杂操作,全在 LifecycleOwner(如 Activity、Fragment 等 “视图控制器” 基类)中封装好,默默在背后为开发者运筹帷幄,

开发者因而得以在 “视图控制器” 子类中只需一句 getLifecycle().addObserver(GpsManager.getInstance) ,优雅完成 “第三方组件” 在自己内部对 LifecycleOwner 生命周期 “感知”。

除解决一致性问题,这么做还 顺带提供其他 2 个好处

1.规避为 “监听状态” 而注入 “视图控制器” 做法

为监听状态,传统做法是,将 Activity 作为方法参数手工注入,这埋下 “内存泄漏” 隐患 —— 因为团队新手容易因 “这有个 Activity 实例”,而在日后误将其上升为成员变量,并依赖给组件中其他成员。

现如今,我们可直接在组件内部 “点到为止” 监听 LifecycleOwner 状态,从而规避这种不恰当使用。

2.规避为 “追溯事故来源” 而注入 “视图控制器” 做法

发生事故时,传统 追溯事故来源 方式,同是向方法参数直接注入 Activity,这再次埋下 “内存泄漏” 隐患。现如今,三方组件实现 DefaultLifecycleObserver 即可在 “生命周期回调” 方法作用域中 直接根据 LifecycleOwner 参数得知事故源。

如这么说无体会,详见《为你还原一个真实的 Jetpack Lifecycle》 中提供的 GpsManager 案例,本文不再累述。

Jetpack LiveData

LiveData 的存在,主要为了 新手老手都能不假思索遵循 “通过唯一可信源分发消息” 标准化开发理念,以便快速开发过程中 “难追溯、难排查、不可预期” 问题发生概率降低到最低。

LiveData 存在前的混沌世界

LiveData 面市前,我们在 “网络请求回调、跨页面通信” 等场景分发消息,多是通过 EventBus 或 Java Interface 完成。

那这造成什么问题?首先,EventBus 等 “消息总线” 只是纯粹传话筒,它 缺乏上述 “标准化开发理念” 约束,那么人们使用该框架时,容易因 “去中心化” 滥用,造成诸如 “毫无防备收到预期外、不明来源推送”、拿到过时数据、事件源追溯复杂度放大至 n²

且,EventBus 本身缺乏 Lifecycle 加持,存在生命周期管理一致性问题。这是 EventBus 硬伤,也是我拒绝使用 EventBus 最主要因素。

如对上述状况无体会,可具体参考我们在 《LiveData 鲜为人知 身世背景 和 独特使命》 中提供的 “播放器状态全局通知” 案例。

LiveData 为何能解决上述这些问题?

首先,LiveData 是在 Google 希望确立 “标准化、规范化” 开发模式 背景下诞生,因而为达成该艰巨使命,LiveData 被十分克制设计为,**仅支持状态输入和监听,且可基于 “访问权限控制” 来实现 “读写分离”**(protected + mutable)。

这使任何一次数据推送,都可被限制为 “只能单方面从唯一可信源推送而来”(也即所谓 “单向数据流”),从而避免消息同步不一致、不可靠、在事件追溯复杂度 n² 迷宫中白费时间,

也即,无论从哪个 “视图控制器” 发起消息请求,结果最终都由作为 “唯一可信源” 的单例或 SharedViewModel 在其内部统一决策、一对多通知

且,这种承上启下方式,使单向依赖成为可能:单例无需通过 Java Interface 回调通知视图控制器,从而规避 “视图控制器” 被 “生命周期更长的单例” 依赖而埋下内存泄漏隐患。

LiveData 有个坑需要注意

不过我个人认为,LiveData 的 Observer 设计缺乏边界感,

作为表现层承担 BehaviorSubject 职能的组件,应避免开发者直接接触 Observer 回调,并确保 “与控件属性一对一绑定”,不然开发者容易将其误用作 “一次性事件分发组件”,造成 “订阅时被自动回推脏数据”;或是开发者误使同一控件实例出现在多个 Observer 回调中,造成《MVI 存在意义》篇 “响应式编程漏洞” 一节所说的 “数据一致性” 问题,

经过广泛实践,发现 DataBinding 的 ObservableFiled 能做到 “与控件属性一对一绑定” 从而完美胜任表现层 BehaviorSubject 工作,因而最终决定将 LiveData 往领域层 “一次性数据分发” 的方向改造为 UnPeekLiveData,使其专职 PublishSubject,

当然,由于 LiveData 存在的初衷并非是专业的 “一次性事件分发组件”,改造过的 UnPeekLiveData 也只适用于 “低频次数据分发(例如每秒推送 1 次)” 场景,

因而若想满足 “高频次事件分发” 需求(例如每秒推送 5 次以上),请改用或参考专职 “领域层” 数据分发的 MVI-Dispatcher 组件,该组件内部通过消息队列设计,确保不漏掉每一次推送。

注:BehaviorSubject 和 PublishSubject 是 “响应式编程” 领域的概念,具体可参考《MVI 存在意义》篇 的解析

Jetpack ViewModel

ViewModel 的存在,主要为了解决 “状态管理” 和 “页面通信” 问题。

ViewModel 存在前的混沌世界

ViewModel 本职工作是 状态托管状态管理 “分治”,也即当视图控制器重建时,

对于轻量状态,可通过 “视图控制器” 基类 saveInstanceState 机制,以序列化方式完成存储和恢复。

对于重量级状态,例如通过网络请求得到的 List,可通过生命周期长于视图控制器的 ViewModel 持有,从而得以直接从 ViewModel 恢复,而不是以效率较低的序列化方式。

在 Jetpack ViewModel 面市之前,MVP 的 Presenter 和 MVVM - Clean 的 ViewModel,由于生命周期短于视图控制器,它们顶多为 DataBinding 提供状态托管,而无法实现状态分治。

到了 Jetpack 这版,ViewModel 以精妙设计,达成状态管理,及可共享作用域。

ViewModel 为何能做到这几点?

其实这版主要基于 工厂模式,使 ViewModel 被 LifecycleOwner 所持有、通过 ViewModelProvider 来引用

所以 它既类似于单例:
—— 当被作为 LifecycleOwner 的 Activity 持有时,能脱离 Activity 旗下 Fragment 生命周期,从而实现作用域共享,

实际上又不是单例:
—— 生命周期跟随作为 LifecycleOwner 的视图控制器,当 Owner(Activity 或 Fragment)被销毁时,它也被 clear。

此外,出于对视图控制器 “重建” 考虑,Google 在视图控制器基类中通过 retain 机制对 ViewModel 进行保留。

因此,对于 “作用域共享” 和 “视图重建” 情况,状态因完好被保留,而得以被视图控制器在恢复时直接使用。

再者,由于存在 “共享作用域” 考虑,ViewModel 本身也承担了跨页面通信职责。此场景下 LiveData “数据倒灌” 问题,上文已介绍,不再累述。

Note:截至 2020.2.1,ViewModel 在 Fragment 中 retain 设计已发生剧变,具体缘由可参考我们在 《页面开发 左右逢源 Jetpack ViewModel》 文末及评论区最新补充。

Jetpack DataBinding

DataBinding 的存在,主要为了解决 “View 实例 Null 安全” 一致性问题。

DataBinding 存在前的混沌世界

DataBinding 面市前,我们若要改变视图状态,唯有先调用该 View 实例,如 textView.setText( ),

这造成什么问题?

当页面存在横、竖布局,且两种布局控件存在差异,例如横屏存在 textView 控件,而竖屏没有,那么我们便不得不在 “视图控制器” 中为 textView 做判空处理,这就造成一致性问题 —— 容易疏忽而忘记判空,毕竟页面多达数十个、每个页面调用控件的地方也无数。

那怎么办?

DataBinding 就是来解决这些问题

通过让 “控件” 与 “可观察数据” 发生绑定,那么当该数据被 set 新内容时,被绑定该数据的控件即可被通知和刷新。

Note 2020.4.18:这一切都是 “编译时自动生成中间代码” 在背后完成的逻辑衔接,也即控件如存在于布局中(例如竖屏布局中)且绑定了可观察数据,就会被调用和通知,如不存在(例如横屏布局中),就没被调用,无论哪一种情况,都不至于发生 Null 安全一致性问题。

换言之,使用 DataBinding 后,唯一的改变是,你无需 “手工调用 View 实例” 来 set 新状态,你只需 set 可观察数据本身。

因而,DataBinding 并非许多人不假思索认为的,将 UI 逻辑搬到 XML 中写、从而难以调试 —— 事实并非如此:

**DataBinding 只负责绑定数据、负责 “作为 UI 逻辑末端状态” 的改变**(也即它是一个不可再分原子操作,本就不需调试),原本在视图控制器中 UI 逻辑怎么写,现还是怎么写,只不过不再需要 textView.setText(xxx),而是直接 xxx.set( )。

所以在 DataBinding 帮助下,好处总共多少个?

1.规避 View 实例 Null 安全一致性问题 —— 无需手工判空。

2.规避 View 实例 Null 安全一致性问题,乃至无需手动调用 View,从而完全不用写 findViewById。

3.就算要调用 View,也不用 findViewById,而是直接通过 mBinding 调用。

4.先前 UI 逻辑基本不用改动,改的只是 “改变末端状态” 方式。

……

此外,DataBinding 有个大杀器:能为控件提供自定义属性的 BindingAdapter,它不仅可解决圆角 Drawable 复用问题,还可实现 imageView 直接绑定 url 等需求,总之,没有它办不到,只有你想不到,DataBinding 好处等着你挖掘。

关于 DataBinding 注意事项、屡试不爽排坑技巧,以及独家解析 DataBinding 严格模式,可具体参考 《从被误解到 “真香” Jetpack DataBinding》,这里不做累述。

综上

Lifecycle 的存在,主要为了解决 “生命周期管理” 一致性问题

LiveData 的存在,主要为了实现 “消息分发可靠一致”

ViewModel 的存在,主要为了解决 “状态管理” 一致性问题

DataBinding 的存在,主要为了解决 “View 实例 Null 安全” 一致性问题

它们的存在,大都为在 “软件工程” 背景下解决一致性问题、将易出错操作封装于后台,方便使用者 “快速、稳定、不产生预期外错误” 编码

全文完

本文配套项目

GitHub : Jetpack-MVVM-Best-Practice

版权声明

本文以 CC 署名-非商业性使用-禁止演绎 4.0 国际协议 发行。

Copyright © 2019-present KunMinX

文中提到的 “xxx 架构组件的存在,是为了在 多人协作软件工程背景下 解决 xxx 一致性问题”,以及 “LiveData 在页面通信、事件回调场景下发生 数据倒灌” 等多处 对特定现象及其本质匹配和概括,均属于本人独立原创成果,本人对此享有所有权和最终解释权。

当您借鉴或引用本文 引言、思路、结论进行二次创作,或全文转载时,须注明链接出处,否则我们保留追责权利。

未经与作者本人当面沟通许可,不得将文章内容用于洗稿、广告包装等商业用途。