前言
Android 开发的架构模式最流行的莫过于 Jetpack 架构组件提供的强大易用的 MVVM 实现;去年公司要重构一块老旧的重要业务,原先的 Java + 无架构实现被我们全面切换到 Kotlin + Coroutines + Jetpack AAC。总体效果令我们颇为满意,也没有发现什么明显的缺陷与短板
Jetpack AAC 虽然很赞,但它不能用于 KMM,于是我们在开源社区找到了一个“替代品”——MVIKotlin
MVIKotlin 是一款实现 MVI 模式的框架,它不仅能用于 KMM,还能用于 JavaScript、JVM、LinusX64、MacX64 等多个 Kotlin Target
MVI架构模式
MVI表示的是Model-View-Intent.这个模式最近才被引入Android;受到Cycle.js框架的思路影响,是基于单向圆柱流的原理进行工作的
● Model:
不像其它架构模式的Model,在MVI架构中Model表示UI的状态。举个例子,UI可能会存在不同的状态,比如数据加载Data Loading,加载完成Loaded,用户的动作造成数据的改变,错误,用户当前屏幕位置状态等等,每一个状态都被存储到Model对象中
● View:
在MVI中View作为接口,可以在Activity或者Fragment中实现。这个接口的意思就是需要有一个容器来接收不同状态并进行展示。它们使用可观察的Intent(这里的Intent并不是传统的Android Intent,这里应该取Intent的本意,意图)来回应用户的动作
● Intent:
这个并不是之前所提到的Android Intent.用户的动作的结果作为输入值传递给Intent。回过来,我们可以说,我们将会发送Models作为输入给Intent,然后通过View加载这些Model的状态
何为MVI?
MVI即Model-View-Intent,它受Cycle.js前端框架的启发,提倡一种单向数据流的设计思想,非常适合数据驱动型的UI展示项目
● Model: 与其他MVVM中的Model不同的是,MVI的Model主要指UI状态(State);当前界面展示的内容无非就是UI状态的一个快照:例如数据加载过程、控件位置等都是一种UI状态
● View: 与其他MVX中的View一致,可能是一个Activity、Fragment或者任意UI承载单元;MVI中的View通过订阅Intent的变化实现界面刷新(不是Activity的Intent、后面介绍)
● Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model进行数据请求
单向数据流
用户操作以Intent的形式通知Model => Model基于Intent更新State => View接收到State变化刷新UI
数据永远在一个环形结构中单向流动,不能反向流动
这种单向数据流结构的MVI有什么优缺点呢?
优点
● UI的所有变化来自State,所以只需聚焦State,架构更简单、易于调试
● 数据单向流动,很容易对状态变化进行跟踪和回溯
● state实例都是不可变的,确保线程安全
● UI只是反应State的变化,没有额外逻辑,可以被轻松替换或复用
缺点
● 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
● state是不变的,每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销
● 有些事件类的UI变化不适合用state描述,例如弹出一个toast或者snackbar talk is cheap, show me the code
单向数据流的不变性
● 咱们的View层应该有一个相似view.render(...)的方法
● 咱们的Model是不可变的,所以不可直接修改Model
● View的渲染有且只有一个来源:即业务逻辑
点击事件 下沉 到业务逻辑层。业务逻辑知道当前的Model(例如,持有一个私有的成员Model,它表明着当前的状态), 这以后根据旧的Model,建立一个新的带有增量/减量值的Model
单向数据流,业务逻辑做为单一源用于建立不可变的Model实例,但对于一个计数器来说未免有点小题大作,不是吗?诚然,是的,计数器只是一个简单的应用程序;
大多数应用程序都是以简单的应用程序开始,但复杂性增加很快——从个人角度来看,单向数据流和不可变模型是必要的,这会使简单的应用程序,在复杂性递增的同时,依然保持着简单(对开发者而言)
可调试和可重现的状态
单向数据流保证了咱们的应用程序易于调试。下次咱们从Crashlytics得到崩溃报告时,咱们能够轻松地重现并修复此崩溃,由于全部必需的信息都已附加到崩溃报告中了
什么叫作必需的信息?
那就是当前的Model和用户用户在崩溃发生时想要执行的操做(好比,点击减量按钮)。这就是咱们重现此次崩溃所需的所有信息,这些信息很是容易收集并附加在崩溃报告中
若是没有单项数据流(好比,对EventBus的滥用,或者将CounterModels的私有域暴露出来),或者没有不变性(这会致使咱们不知道谁实际更改了Model),那么bug的复现就没那么容易了
为什么是MVI?
由于没有明确的状态管理,随着应用程序的增长或添加功能或事先没有计划的功能,视图渲染和业务逻辑可能会变得有点混乱,老实说,这种情况经常发生,很少从项目规格一开始就清楚和全面定义所有功能,应用程序代码库的可扩展性越强,接受新想法和更新就越灵活
状态危机导致业务逻辑和 UI 渲染纠缠不清,这是怎么发生的?
● Presenter/ViewModel有自己的状态
● 业务逻辑产生自己的状态
● 尝试同步上述两种状态
● 如果对Presenter/ViewModel的输入没有明确的管理->处理输出的纠结结果->混乱的业务逻辑和视图渲染->代码闻起来像碎片/活动变成了一个黑洞类(通过吸引更多责任)或有不同的类(当您想更新,但由于许多不同的原因不得不以多种不同的方式更改类时),这是由于关注的分离不良
调整MVI架构模式有什么好处?
● 状态管理利用不变性来拥有单一的真实来源
● 单向数据流
● 可复制状态->简单代码重用
● 更好地分离关注->可维护性
MVI 模式的改动
MVI 模式的改动在于将 View 和 ViewModel 之间的多数据流改为基于 ViewState 的单数据流MVI 将代码分为以下四个部分:
● View: Activity 和 Layout XML 文件,与 MVVM 中 View 的概念相同
● Intent: 定义数据操作,是将数据传到 Model 的唯一来源,相比 MVVM 是新的概念
● ViewModel: 存储视图状态,负责处理表现逻辑,并将 ViewState 设置给可观察数据容器
● ViewState: 一个数据类,包含页面状态和对应的数据
在实现细节上,View 和 ViewModel 之间的多个交互(多 LiveData 数据流)变成了单数据流。无论 View 有多少个视图状态,只需要订阅一个 ViewState 便可以获取所有状态,再根据 ViewState 去响应。当然,实践中应该根据状态之间的关联程度来决定数据流的个数,不应该为了使用 MVI 模式而强行将多个无关的状态压缩在同一个数据流中。
● 唯一可信源: 数据只有一个来源(ViewModel),与 MVVM 的思想相同
● 单数据流: View 和 ViewModel 之间只有一个数据流,只有一个地方可以修改数据,确保数据是安全稳定的。并且 View 只需要订阅一个 ViewState 就可以获取所有状态和数据,相比 MVVM 是新的特性
● 响应式: ViewState 包含页面当前的状态和数据,View 通过订阅 ViewState 就可以完成页面刷新,相比于 MVVM 是新的特性
但 MVI 本身也存在一些缺点:
● State 膨胀: 所有视图变化都转换为 ViewState,还需要管理不同状态下对应的数据。实践中应该根据状态之间的关联程度来决定使用单流还是多流
● 内存开销: ViewState 是不可变类,状态变更时需要创建新的对象,存在一定内存开销
● 局部刷新: View 根据 ViewState 响应,不易实现局部 Diff 刷新,可以使用 Flow#distinctUntilChanged() 来刷新来减少不必要的刷新
不过,MVI 并不是一个全新的设计模式,其背后设计理念与 Redux 模式如出一辙;在 Redux 里完全可以找到与 MVI 相同的各个要素,而且明显 Redux 的命名方式更加清晰无歧义
这个架构的核心思想
● 我们在MVVM架构中包括一个实际的不可变的Model层,我们的视图依赖于这个Model的状态变化。这样一来,ViewModel就必须修改和公开这个单一的Model
● 为了避免冗余和简化这种架构在多个地方的使用,我创建了两个抽象类,一个用于我们的视图(为Activity、Fragment、自定义视图分开),一个用于ViewModel
AacMviViewMode;一个通用的基类来创建ViewModel;它需要三个类STATE、EFFECT和EVENT
如你所见,我们有viewState。STATE和viewEffect。EFFECT和两个私有的LiveData容器viewStates。MutableLiveData 和viewEffect: SingleLiveEvent ,它们通过公共函数viewStates()和viewEffects()被暴露出来;请注意,我们正在扩展AndroidViewModel,因为它将允许我们在需要时使用应用程序上下文(仅);此外,我们正在记录每个viewEvent,我们将处理这些事件
结语
MVI模式为了解决什么问题?结论:为了在MVC架构模式的思想上实现响应式编程范式;MVC主要的目的是将View和Model隔离;MVI在隔离View和Model的基础上,实现了响应式编程(也就是reactive编程)
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
PS:有问题欢迎指正,可以在评论区留下你的建议和感受; 欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下
网友评论