Jetpack 领域层组件拆解及改善建议

8,454 阅读5分钟

Jetpack 从 2017 年问世至今,相信多数开发者都已熟悉 “业务架构组件” 使用,

关于部分架构组件的设计,笔者留意到其潜在的隐患,经过多年沉淀,如今觉得是时候和大家分享,并给出改善建议,相信阅读后你会豁然开朗。

弊病 1:违背原则

根据 wiki 百科的介绍,“最佳实践” 是一管理学概念,意指 “认为存在某种技术、方法、过程、活动或机制可使生产或管理实践的结果达到最优,并减少出错的可能性”,

易知 “架构模式” 即 “软件工程” 领域 “最佳实践”。

软件工程实践有其原则,即 “设计模式 6 大原则”。笔者认为,“单一职责原则” 是 6 原则之首,宁违背其他原则,也不可违背此原则 ——

单一职责原则与 “本质” 直接挂钩,架构组件设计如违背单一职责原则,是后续所有弊病祸根所在。如这么说无体会,那就继续往下,一睹为快,

弊病 2:职责混杂

Jetpack ViewModel 本质有二,

一是 “页面状态容器”,使页面旋屏重建后可直接从 ViewModel 拿取数据,免去传统 onSaveInstanceState、onRestoreInstanceState 之苦,

二是 “作用域可设定容器”,例如作用域设定为 Activity 级,则 Activity 旗下 Fragments 皆可共享 Activity 级 ViewModel 之公共数据。

由此易得,该框架违背单一职责原则。

但其实,这俩功能都有存在意义。那怎办,此时最佳实践即,创建子类 “继续划分职责”,直至各司其职

例如可将 ViewModel 划分为 State-Holder 和 Result-Handler,

前者专职托管页面状态数据,是表现层组件,可作为静态内部类声明于 Activity/Fragment 中,作用域仅限该页面本身,为该页面专属;

image-20230514104736199

后者专职业务逻辑处理及结果统一分发,是领域层组件,可为多个 Activity/Fragment 复用。

image-20230514104758116

也因此,State-Holder 为 “表现层” 缓存,Result-Handler 为 “领域层” 消息源,数据来去之流程,总是从页面发起 Event,交由 Result-Handler 处理并回传 Result 至页面。

页面根据 Result 的性质来区分处理:对于一次性事件 Event,直接执行, 对于状态 State,交由 State-Holder 中的 State 组件托管,并通知所绑定的控件完成渲染。

image-20230514104811564

由此,当页面旋屏重建时,表现层仍是从 State-Holder 读取缓存完成渲染,而领域层 Result-Handler 在完成自己那一环推送后,不应越界干涉、自动重推(replay),以免好心办坏事。

—— 正常流程就该是这样。

弊病 3:缺失完备

完备的架构应当包含完整的 “表现层 + 领域层 + 数据层” 三层,

其中 表现层专职 “请求的发送” 和 “结果的响应”,

领域层专职 “业务逻辑处理” 和 “结果的推送”,

数据层专职 “数据逻辑处理和回推”,

官方示例默认将完备的三层架构削减为两层,直接在 ViewModel 中处理业务逻辑和回推结果,这造成页面中至少需要准备两个出口,一个是事件回推出口,一个是状态回推出口,

也即无法通过统一的方式将 State 和 Event 串流回推、然后在页面中从 “唯一的出口” 响应并分流处理,

—— 出口多个,则易埋下 “数据不一致” 隐患。例如同事缺乏 State 和 Event 意识,误在 State 回调中混入 “一次性事件的响应”,

image-20230514104854670

弊病 4:过度暴露

LiveData 是借鉴 “响应式编程” BehaviorSubject 的设计,当 Observer 订阅 LiveData 时,会自动收到最后一次状态,

不幸的是,LiveData Observer 的设计缺乏边界感,其 Observer 回调暴露给开发者,这也就导致 “回调中出现的内容不可预期”,

例如对 “响应式编程” 不熟的同事,可能直接在 Observer 回调中处理 “一次性事件”,比如 show Toast,那么环境发生变化时,会再次 show Toast,此举不符预期,

又或者,同事可能让同一个控件实例出现在多个 Observer 回调中,这便又埋下页面重建时 “收到不符预期脏数据” 的隐患。

注:1.此处场景示例可参见《MVI 存在意义》篇 “响应式编程的漏洞” 一节的演示。

2.目前唯 DataBinding ObservableField 能做到对开发者屏蔽 Observer 回调。

image-20230514104955375

使用 MVI-Dispatcher 承担 Result-Handler

一个完备的领域层消息分发组件,至少应当满足以下几点:

1.内含消息队列,可入栈和出栈每一个消息,不漏掉消息,

2.页面不可见时,队列暂存期间发来的消息,直至页面重新可见时,将消息消费。

MVI-Dispatcher 应运而生。

除了上述基本设定,MVI-Dispatcher 改进和优化还包括:

1.可彻底消除 mutable 样板代码,一行不必写

2.可杜绝团队新手滥用 mutable.setValue( ) 于 Activity/Fragment

3.开发者只需关注 input、output 二处,从唯一入口 input 注入 Event,并于唯一出口 output 观察

4.团队新手在不熟 LiveData、UnPeekLiveData、SharedFlow、mutable、MVI 情况下,仅根据 MVI-Dispatcher 简明易懂 input-output 设计亦可自动实现 “响应式” 开发

5.可无缝整合至 Jetpack MVVM 等模式项目

1.遵循 “单一职责原则”:

MVI-Dispatcher 仅用于承担上文所述 Result-Handler 设计,消除只读 Result 分发模型中 mutable 样板代码,故可无缝整合至 Jetpack MVVM 等模式项目。

2.遵循 “迪米特原则/最小知道原则”:

通过 “内聚”,将 LiveData 内嵌于 Dispatcher,且将 mutable.setValue 屏蔽于 protected sendResult 中。由此从根源上杜绝开发者于 Activity/Fragment 误用滥用 setValue,确保消息来源一致。

且其简明易懂 input - output 设计,使开发者只需关注 input、output 这两处,从唯一入口 input 处注入 Event,并在唯一出口 output 处观察 Result

3.引入队列设计

MVI-Dispatcher 通过 Result 定长队列支持事件连发,

Result 随取随用,用完即走,无内存溢出隐患,且绝不丢失事件,

具体可根据 ComplexRequest 等案例测试,观测 Logcat 实际输出。

Github: MVI-Dispatcher

Github: MVI-Dispatcher-KTX(for Kotlin only)

最后

天下本就无完美事物,有则是创作者精益求精、各路贤人能者集思广益。