前言
MVI并非新兴事物,在2020年时亦曾有通过撰写一篇文章与诸位读者探讨一二的念头。
当时项目采用MVP分层设计,组员的代码风格差异也较大,代码中类职责赋予与封装风格各成一套,随着业务急速膨胀,代码越发混乱。试图用 MVI架构
+ 单向流
形成 掣肘
带来一致风格。
但这种做法不够以人为本,最终采用 “在MVP的基础上进行了适当改造+设计约定的方式” 解决了问题,并未将MVI投入到商业项目中,于是 放弃了纸上谈兵。
在半年前终于有机会在商业项目中进行实践,同诸位谈一谈使用后的 个人感悟 ,并藉此讲透MVI等架构。
所有内容将按照以下要点展开:
- 从架构的理念出发 – 简单列明各种
MVX
的理念 , MVX:指代 MVC、MVP、MVVM、MVI - 拥抱复杂的同时实现简化 – 通过对比理解单向数据流动所解决的痛点、设计Intent的原因等问题
- 单一可信数据源,不可僵化信奉
- 要想优雅,需要工具 – 借助声明式、响应式编程工具,构建
流
,屏蔽命令式编程中的细节,同样是聚焦和简化 - 状态和事件分家,绝不是吃饱了撑的 – 为什么要裂变出状态和事件,如何界定
内容会很长,我会酌情再写一些 解
,结合实例和代码演示内容。
两个项目的基本情况
相比于之前的巨型项目,这两个项目的业务量均不大,一个是基于蓝牙和局域网的操控类APP,下午简称APP-A,一个是内部使用的工具,分析公司各个产品的日志,简称APP-B。
虽然他们的业务深度要比一般的APP要深,但在 本质上一致 ,毕竟同类型业务量再多也仅仅是重复运用一套模式 ,并不影响本质。
和诸多项目的本质一致,均符合如下图所示的逻辑分层,并在人机交互过程中执行业务逻辑:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1u5uz9FQ-1661322561053)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b226761ddc34491996ebe199d2238b6~tplv-k3u1fbpfcp-watermark.image?)]
- APP-A 是Android项目,图方便纯kotlin
- APP-B 是 Compose-Desktop项目,不得不kotlin
过于絮叨了,我们进入正文。
从架构的理念出发
谨记,实际情况中,MVI、MVVM这些架构均先由Web应用领域提出,用于解决浏览器Web应用研发中的问题。
在后续的应用领域发展过程中,存在共性问题,便引入了这些设计,并结合自身特点进行了拓展。
接下来我们聊一聊理念,不比武功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rNtjyHrJ-1661322561054)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e22a5bc38798404e959a6533a384adae~tplv-k3u1fbpfcp-watermark.image?)]
图片出自电影一代宗师
MVI的理念
MVI
脱胎于 Model View Intent
- Intent:驱动model发生改变的意图,以UI中的事件最为常见;
- Model:业务模型,包含数据和逻辑,是对应
客观实体
的程序建模
; - View:表现层的视图,以UI方式呈现Model的状态(以及事件),接受用户输入,转换为UI事件
官方的这幅图很好的呈现了三者之间的驱动关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ep8Uej99-1661322561054)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05d56842e0964f388f41fd6e27825849~tplv-k3u1fbpfcp-watermark.image?)]
这张图非常简单,它摒弃了驱动方式的细节,只体现了角色与驱动关系。
注意,只要设计中满足 角色和驱动关系
符合上图,就是MVI架构设计,并不限制 驱动方式的实现细节
经典的MVI驱动细节要比上图复杂很多,下文再聊。
从软件设计的原则出发:职责分离并封装
的目的是 解耦
、 可独立变化
、复用
。
显然,区别于 MVVM
、 MVP
、 MVC
,角色上的差别在于 ViewModel、Presenter、Controller、Intent四者,而它们又是View和Model之间的纽带。除此之外,V和M亦稍有不同。
MVC、MVP
MVC、MVP 中,C和P的职责体现为 控制、调度
。
MVP中 V
和 M
完全解耦可独立变化,MVC中 M
直接操作 V
耦合高,在web应用中,C
需要直接操作DOM。
MVVM
MVVM中,提倡 数据驱动
, 数据源
被剥离到 VM
中,在 双向绑定框架
的加持下,View层的输入反映为数据的变化,数据的变化驱动视图内容。
显然,VM的职责限于维护数据状态,如有必要,驱动View层消费数据状态, 不必再关注如何操作视图。
一般来说,双向绑定框架已经引入观察者模式实现,可响应式驱动,VM一般没有必要关心 响应式驱动和下游观察者生命周期问题
简单思考之后会发现MVVM的问题,它的侧重点在于 利用双向绑定让开发者专注于数据状态的维护,从操作视图更新中得以解放
,它难以解决 无天然状态
问题,例如:按钮点击这类事件。
MVI
在MVI中,结合业务背景将UI事件等内容转换为 Intent
,驱动Model层业务,Model层的业务结果反映为 视图状态
+ 事件
。
因此View层和Model层之间已经解耦,并可以吸收MVVM中的优点采用如下设计:
- 将双向绑定退化为单向绑定,View层消费UI状态流和事件流,这也意味着UI状态的职责精简,它不再承载View层的用户输入等事件
- 将UI状态独立,Model层仅产生
UI状态的局部变化
和事件
下图为经典的MVI原理示意图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ag8in4l-1661322561055)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9dc25d054bd4693a23714e090af4476~tplv-k3u1fbpfcp-watermark.image?)]
在上文中,我们已经讨论了各个角色的职责,下面逐步展开讨论角色具备的特性和细节知识。
在此之前,还请谨记:合适的才是最好的
没有绝对的最好的设计,只有最合适的设计。
再好的架构,都需要遵循其理念并结合项目因地制宜地进行调整,以获得最佳使用效果。所以请读者诸君务必在阅读时,结合自身项目的情况仔细思考以下问题:
- 引入新框架所解决的痛点、衍生的问题、是否需要进行框架调整?
- 框架中的角色功能,为什么出现,又有怎样的局限?
单向数据流动
MVI拥抱了结构复杂,但能够灵活应对业务编码时的各种情况,按部就班即可。
从MVI原理图中,可以清晰的看到 “数据” 的流动方向。
起始于 Intent
,经过分类和选择性消费后产生 Result
,对应的reducer函数计算后,得到最新的 State
(以及裂变出必要的 Event
,图中未体现) ,驱动视图。
注意:
单向
是指 单一方向- 此处的
数据
是广义的、宽泛的。 - 仅描述数据流的 变化方向 ,与数据流的数量无关,但一般 形成有效工作 均需要两条数据流(上行数据流和下行数据流)
即驱动数据流变化的方向是唯一的,在英文中的术语为:Unidirectional Data Flow
简称 UDF
。
MVC、MVP中的痛点
前文我们提到,在MVC和MVP中,着眼于 控制、调度 ,并不强调 数据流
的概念。
View和Model间之间的交互,一般有两种编码风格:双向的API调用、单向的API调用+回调:
注意:以下两图并未体现Controller和Presenter细节,仅表意,从View层出发的API调用和回到View层的UI更新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-raPCbsF1-1661322561056)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a48429062dc0450689ffe61248694d34~tplv-k3u1fbpfcp-watermark.image?)]
双向API调用如上图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jinHZxl2-1661322561056)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5795fa2dbe0643a1a21cddc6bdd59824~tplv-k3u1fbpfcp-watermark.image?)]
单向API调用+回调更新UI如上图。
显而易见,这两种方式无法继续抽象,需根据实际业务进行命令式编码。当UI复杂时,难以写出清晰、易读的代码,维护难度激增。
MVVM解决UI更新代码混乱问题
前文我们已经提到:MVVM中通过绑定框架,将UI事件转化为数据变化,驱动业务;业务结果表现为数据变化,驱动UI更新。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6i929K10-1661322561057)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7a30f35aafe41a7bee581ba03363114~tplv-k3u1fbpfcp-watermark.image?)]
显而易见,维护朴素的数据要比直接维护复杂的UI要简单。
但问题也同时产生,data1的变化有两个可能的原因:
- Model层业务结果使其变化,并期望它驱动UI更新
- View层发生事件,反馈数据变化,并期望它驱动Model层逻辑
因此,框架需要考虑标识数据变化来源、或者其他手段消除方向性所带来的问题。
并且MVVM难以灵活决定的 “何时调用Model层逻辑”,即大多数业务中,都需要结合多个属性的变化形成组合条件来驱动Model层逻辑。
本篇并不重点讨论MVVM,故不再展开MVVM解决循环更新的方案,以及衍生的问题。
尽管如此,MVVM中的数据绑定依旧解决了View层更新繁杂的问题。
用Intent灵活决定何时调用Model
既然数据驱动UI有极大的益处,且View层事件驱动ViewModel的数据变化有很多弊端 (需要建立很高的复杂度) ,那自然需要 趋利避害
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-574JyZra-1661322561058)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/751562b00d3b418cbe28fa793442967d~tplv-k3u1fbpfcp-watermark.image?)]
仅保留数据驱动UI的部分,并增加Intent用以驱动Model层业务
在于 MVC/MVP
以及 MVVM
对比