往期回顾,专栏目录,更新动态,优惠政策,版权须知
温馨提示:如这是第一次接触《重学安卓》,可通过上述链接快速了解《重学安卓》专栏,获取 专栏目录、试读内容、更新动态 和 发展状况。
截至目前,专栏已对 体系化文章 实施 3310 余次修订,数十位群友告诉我,受专栏启发 亦开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创开源库 和 提供内推机会,订阅后可随时进群交流。
·
重要提示:
阅读本文最佳时机是,您已吃过 阅读 Jetpack MVVM 相关源码时 找不到头绪的苦。
您还没吃过苦,那您先不要着急阅读本文。您得吃过苦,才会有体会。
在您吃够这方面苦后,您才有机会发现,本文正是专用于解决 “如何找到正确打开方式” 的困扰。
我们绝不通篇贴源码,而是基于广泛实践和反思,在累积过大量样本、乃至足以排除掉所有干扰信息后,点到为止揭露 “Jetpack MVVM 分层设计” 的来龙去脉,方便您理解其真实的存在意义,乃至笃信使用项目中。
前言
关于 Jetpack 架构,相信多数小伙伴都是先根据官网资料学习,
等到实际项目坑踩多、对各种痛点有体会了,便有动力继续从 “使用成本、稳定性” 等角度出发,探索相对均衡的架构方案。
为此在正式介绍本专栏配套项目 Jetpack MVVM 方案设计前,我们一起来回顾下流行写法的由来和优缺点。
文章目录一览
- 前言
- 流行示例的发展史
- 为何要设计成这样子
- 响应式编程的好处
- 响应式编程的漏洞和关键细节
- 通常是怎么解决的
- 两层架构的不便之处
- 从源头把问题消灭
- 响应式编程的观察者设计
- 什么是 “缺乏边界感的设计”
- 通过 “单一职责原则” 迎刃而解
- Jetpack MVVM 三层架构
- 分层,各司其职
- Note 2023.09.07 加餐
- 灵活和符合预期
- 统一管理的业务逻辑
- 综上
- Note 2023.06.13 加餐:
- 有效区分 UI 逻辑 & 业务逻辑
流行示例的发展史
目前网上流行示例多是遵照 “官网架构示例” 来写,也即 “表现层”、“数据层” 两层架构。
其中 Activity/Fragment、Jetpack ViewModel 属于表现层,DataRepository 等属于数据层。
表现层的每个页面配备一个 Jetpack ViewModel,在 Jetpack ViewModel 中托管当前页面的 UiStates,并以 “响应式编程” 的方式完成业务逻辑。整个流程正如下图所示:

为何要设计成这样子
那么为什么设计为两层架构,为什么业务逻辑在 ViewModel 中写,并以 “响应式编程” 方式?
如果我是官网,我大概率也会这样设计,因为官网的定位是提供入门信息,以让初学者知道 “总共都有些什么” 为目的,
那么出于篇幅和完备性考虑,需要将尽可能多的东西压缩在较小篇幅里,让开发者一览无余。如此也就促成了 “两层架构” 设计:
本该在领域层组件中写的业务逻辑,被整合到表现层 ViewModel 中写,且出于完备性考虑 —— 为了支持单元测试,业务逻辑改为 “响应式编程” 的方式来写。
响应式编程的好处
关于什么是 “响应式编程”、其好处和漏洞,我们在《响应式编程和 MVI》篇已详细介绍过,此处简要复述下:
响应式编程特点:暗示人们应当总是向数据源请求数据,并 在指定观察者中响应数据的变化。
也即,控件实例的渲染代码(比如 textView.setText),只在 LiveData 的观察者回调中,统一根据数据源传回的数据进行渲染,
这样做的好处是,便于单元测试:有输入必有回响。反之如像往常一样,将控件渲染代码分散在观察者以外的各个方法中,便很难做到这一点。
响应式编程的漏洞和关键细节
随着业务发展,人们开始往 “粘性观察者” 回调中添加各种控件渲染,
如果同一控件实例(比如 textView)出现在不同粘性观察者回调中:

假设用户操作使得 textView 先接收到 liveData_B 消息,再接收到 liveData_A 消息,
那么旋屏重建后,由于 liveData_B 的注册晚于 liveData_A,textView 被回推的最后一次数据反而是来自 liveData_B,给用户的感觉是,旋屏后展示老数据,不符预期。
由此可得,响应式编程存在 1 个不显眼的关键细节:
一个控件应当只在同一个观察者中响应,也即同一控件实例不该出现在多个观察者中。
但如果这么做,又会产生新的问题。由于页面控件往往多达十数个,如此观察者也需配上十数个。
通常是怎么解决的
所以,是否存在某种方式,既能杜绝 “一个控件在多个观察者中响应”,又能消除与日俱增的观察者?
由于官方需要兼顾简单和完备性,乃至需要以保留 “两层架构” 的设计为前提。于是改善方案即,通过 “将页面状态聚合” 来统一消除上述 2 个问题。
也即 2022 年起,官方采取 MVI 风格做法 —— 将页面所有 UiState 都聚合到一个 data class 中,并通过唯一的 LiveData 来托管该 UiStates,如此便可达成 “一个控件只在同一个观察者中响应”。
两层架构的不便之处
两层架构的 “好处” 前面我们已经说了,就是为了入门时压缩篇幅,让架构图简洁明了和一览无余,
但实际开发过程中,容易体会到两层架构的不便之处,
例如开发一款笔记软件,包括列表页和详情页,其中列表页包含 “删除、收藏、置顶” 等操作,详情页也包含 “删除、收藏” 等操作,
如果是两层架构写法,列表页和详情页各自准备一个 ViewModel,并且每个 ViewModel 都需将这些业务都写一遍,造成业务的冗余,
如此后续若修改列表页 ViewModel 中如 “收藏” 操作的业务逻辑,极可能疏忽详情页 ViewModel 的业务,也即造成代码修改的不一致。

Note 2022.12.09 加餐:
以下提供伪代码图例,作为上述图例的补充。

从源头把问题消灭
如何从根源解决上述提到的问题?
作为进阶者,我们不妨跳出入门设定,重新审视框架设计的合理性。
对此从软件工程角度出发,笔者在设计模式原则中找到答案 —— 任何框架,只要遵循 “单一职责原则”,便能有效避免各种不可预期问题,反之 “缺乏边界感的设计” 则易引发不可预期问题。