移动应用架构谈 - MVI

307 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

本文为移动应用架构谈系列文章之一,本篇主要为学习思考笔记

尽量地不拘泥于某个平台的具体实现去审视GUI应用程序架构模式

缘分渊源 - Intro

在2022年的尾巴,终于对2021年的尾巴官方提出推荐的应用架构MVI有了初步的学习研究。在研究过程中,看到了图1的函数式结构很妙的体现了MVI的强调的关键设计原则点:

  • 数据模型驱动界面(Drive UI from data models)
  • 单向数据流动(UDF, Unidirectional Data Flow)
  • 单一数据源(SSOT, Single source of truth)

image.png

图1 - MVI 函数

图中View函数的变化仅与传入的Model这唯一参数有关,即通过数据模型来驱动界面的的变化,且只有单一的数据模型来驱动。Model函数的变化又只与传入的Intent这一唯一参数有关,因而导致了整个函数参数依赖关系的流向是从用户意图开始,通过将模型函数生成的模型参数传递给View函数从而引起了界面的变更,整个参数的生成流动方向为单向,即只存在单一的数据流动方向。而Model/View/Intent的缩写简化下来恰好是MVI,似乎MVI的架构已经学完了...

但是,这个架构是

  • 如何定义应用的各个部分之间的界限以及每个部分应承担的职责?
  • 如何做到允许应用扩缩、提升应用的稳健性并且方便对应用进行测试?

人云亦云 - Arch

图2是来自官方文档的典型架构示意图,架构本身主要划分成了两层:界面层和数据层。其中,界面层提供了用户可见的界面,该可见界面可能提供到用户执行其意图的操作,即界面层包含了应用开发的两个核心点,View绘制和事件响应处理。数据层实质上提供了资源管理能力,不管是来自于后端资源提供处理的数据,还是来源于移动硬件本地资源提供处理的数据。当然,图中还包括一个可选的网域层,个人更倾向将网域层理解为数据层的一部分,是为了降低数据层的复杂业务数据组织的一种处理手段。 image.png

图2 - 典型应用架构的示意图

更具体的分层说明来看

  • 界面层

关于界面层绘制的描述简而言之,简言说的不是夏冬说的,就是一个ui组件的最佳实现实践。其实就是最小化暴露出去绘制一个界面的关键属性,即ui state。特别注意ui state的默认状态设计。而如何从数据层到界面层的数据状态生成,可以参考胶水层部分的思考。 image.png

图3 - 界面层应用架构逻辑示意图

至于界面层意图的表达,这里在实现过程中存在一个选择,是一个意图实现一个事件接口,还是同意封装一个事件处理入口。第二中方式在SDK的开发中比较常见,对于业务开发比较陌生,毕竟消息队列对于Android开发来说,不是Handler就是Eventbus,很难有与后端业务复杂度相关的框架使用。

最后,关于界面层在应用开发中应该特别注意,界面层对于生命周期依赖部分的管理。

  • 数据层 数据层的典型实现是包括了Repository和DataSource,加上网域层的用例(Use Case)一起构成了我理解的数据层。

image.png

图4 - 应用架构数据层示意图

对于DataSource,单一职责是其重点,提供单一数据类型的单一数据源,对于返回的数据中可能存在的使用不到的数据模型属性,最好直接裁剪掉,不仅能减少内存占用,而且还能有效降低不必要的测试成本。

基本的Repository可能包含0个或多个DataSource,即0个的时候可能是因为Repository跟DataSource融合成为了一个,这里有一个官方小帖士记忆深刻

注意:通常,如果存储库只包含单个数据源并且不依赖于其他存储库,开发者会将存储库和数据源的职责合并到存储库类中。这种情况下,在应用的更高版本中,如果存储库需要处理来自其他来源的数据,请不要忘记拆分这些功能。

很多时候使用一个架构进行开发,刚开始的时候参照架构添加了所有层级,回头审视的时候会发现存在冗余的层级使用,可能会去优化缩减了层级,但是缩减后不要忘记了层级的拆分扩展,这大概就是学习一个东西入门三阶段。当我们不断练习熟练使用,在直接写业务根据的框架实现优化后效果的时候,不要忘记整个框架的全貌,正常的层级是怎么样的,哪里我们做了优化。

数据层示意图中中间跟右边,可以看到多层存储Repository跟UseCase都存在依赖多个Repository的结构。其区别点在于,多层存储Repository解决的的数据模型聚合,即根Repository的数据模型由子Repository的数据组合而成;Use Case,更多的是一个业务逻辑生成使用过程,可能通过图中Author的数据来控制生成个性化的New数据,更多的是业务逻辑组织层面上的东西。

数据层最重要的莫过于数据模型的设计。落到Android开发实现中,使用kotlin的data类,比起java的数据构建,确实对于业务开发不要太省事儿了。

  • 胶水层

界面层跟数据层的分层抽象,让界面层组件与数据层的组件开发实现有了清晰的职责边界;清晰且有限的输入跟输出,让单元测试的使用也变得更简单;而不同组件的实现,则很好的提供了一个研发协作的机制,让研发资源可以更好的并发执行。然而这其中确乎缺少了点东西,一个应用的业务组件间的通信交互是如何实现的?我们需要

  • 适配器。将数据层的数据与状态,转化成界面层的渲染数据
  • 观察者。监听用户行为,根据行为处理数据状态的变更;根据数据模型的变更,通知界面层重新渲染展示。

那么在Android端,系统库提供的Viewmodel,提供了适配器与观察者工作的场所,而LiveData(MutableLiveData/MediatorLiveData)或Flow,提供了数据流动变化观察的能力。虽然Flow是趋势,但是其使用复杂度远高于Livedata的开发,开发上手难度比较陡也是事实。

小白吐槽 - Sum

应用开发最终还是回到程序设计原点,数据结构加上算法,数据结构即业务模型建模,算法即业务流程的组织。越发体会到开发前做好业务模型的澄清与设计,对提升业务开发的重要性;一个好的数据模型设计,很好的降低了需要特殊业务处理逻辑的风险,若非历史包袱导致,在业务流程感受到流程组织难受的时候,就要好好想想是否是存在业务建模把数据状态放错了位置导致。而业务流程从命令式实现到响应式的变迁,也大大内聚了业务逻辑,将散乱到各处的命令执行聚集在一起,提供了很好的内聚模式,大大提高了业务逻辑的可测试性。

参考

  1. Guide to app architecture
  2. 用Kotlin Flow解决Android开发中的痛点问题