美丽的神话 发表于 2024-8-20 10:47:39

谈一谈在两个商业项目中使用MVI架构后的感悟_mvi viewstate拆分(4)

深知大多数步伐员,想要提升技能,往往是本身摸索成长,但本身不成体系的自学效果低效又漫长,而且极易碰到天花板技能故步自封!
https://img-blog.csdnimg.cn/direct/743b668910224b259a5ffe804fa6d0db.png
https://img-blog.csdnimg.cn/img_convert/2b9f85759a226c69e5f198faf12dfd4f.png
https://img-blog.csdnimg.cn/img_convert/f36e0696440a4b835eaf2d90371a4625.png
既有恰当小白学习的零基础资料,也有恰当3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!
由于文件比较多,这里只是将部分目次截图出来,全套包含大厂面经、学习笔记、源码课本、实战项目、大纲路线、讲授视频,并且后续会持续更新
必要这份系统化的资料的朋侪,可以戳这里获取
MVI

在MVI中,结合业务配景将UI事件等内容转换为 Intent ,驱动Model层业务,Model层的业务效果反映为 视图状态 + 事件。
因此View层和Model层之间已经解耦,并可以吸收MVVM中的优点采用如下设计:


[*]将双向绑定退化为单向绑定,View层消费UI状态流和事件流,这也意味着UI状态的职责精简,它不再承载View层的用户输入等事件
[*]将UI状态独立,Model层仅产生 UI状态的局部变革 和 事件
下图为经典的MVI原理示意图:
https://img-blog.csdnimg.cn/img_convert/5c11391375c89a9de833c425d6baf504.webp?x-oss-process=image/format,png
在上文中,我们已经讨论了各个脚色的职责,下面逐步展开讨论脚色具备的特性和细节知识。
在此之前,还请谨记:合适的才是最好的
   没有绝对的最好的设计,只有最合适的设计。
再好的架构,都必要遵照其理念并结合项目因地制宜地举行调解,以得到最佳使用效果。所以请读者诸君务必在阅读时,结合自身项目标情况细致思索以下问题:


[*]引入新框架所解决的痛点、衍生的问题、是否必要举行框架调解?
[*]框架中的脚色功能,为什么出现,又有怎样的范围?
单向数据流动

   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更新
https://img-blog.csdnimg.cn/img_convert/95c16a8c1e200b053ad9f73531f96152.webp?x-oss-process=image/format,png
双向API调用如上图。
https://img-blog.csdnimg.cn/img_convert/a3a8a6a37dc6ca1c1dfef020ec610897.webp?x-oss-process=image/format,png
单向API调用+回调更新UI如上图。
显而易见,这两种方式无法继续抽象,需根据现实业务举行命令式编码。当UI复杂时,难以写出清晰、易读的代码,维护难度激增。
MVVM解决UI更新代码杂乱问题

前文我们已经提到:MVVM中通过绑定框架,将UI事件转化为数据变革,驱动业务;业务效果体现为数据变革,驱动UI更新。
https://img-blog.csdnimg.cn/img_convert/5e2b71932bf89f075529209363e7a5de.webp?x-oss-process=image/format,png
显而易见,维护朴素的数据要比直接维护复杂的UI要简单。
但问题也同时产生,data1的变革有两个可能的原因:


[*]Model层业务效果使其变革,并期望它驱动UI更新
[*]View层发生事件,反馈数据变革,并期望它驱动Model层逻辑
因此,框架必要考虑标识数据变革泉源、大概其他本领消除方向性所带来的问题。
并且MVVM难以灵活决定的 “何时调用Model层逻辑”,即大多数业务中,都必要结合多个属性的变革形成组合条件来驱动Model层逻辑。
本篇并不重点讨论MVVM,故不再展开MVVM解决循环更新的方案,以及衍生的问题。
尽管如此,MVVM中的数据绑定仍旧解决了View层更新繁杂的问题。
用Intent灵活决定何时调用Model

既然数据驱动UI有极大的益处,且View层事件驱动ViewModel的数据变革有很多毛病 (必要创建很高的复杂度) ,那自然必要 趋利避害
https://img-blog.csdnimg.cn/img_convert/6036d8d8d1143b4704f5a13b56766995.webp?x-oss-process=image/format,png
仅保留数据驱动UI的部分,并增加Intent用以驱动Model层业务
在于 MVC/MVP 以及 MVVM 对比后不难过出结论:


[*]MVC/MVP中,View层通过调用C/P层API的方式最终调用到Model层业务,方式质朴、无难度。但业务量规模增大后接口方法数也会增多,导致C/P层尾大不掉,难以重用。
[*]MVVM中,VM层总是必要使用 本领 举行模型概念转换,以满足业务响应满足现实需求,必要很深厚的设计经验才能写出非常优秀的代码,这并不友好。
作者按:我个人认为一个友好的设计,不应当剑走偏锋,而应当大巧不工,能够以力破法,告竣 “使用者只必要吃透理论就可以解决各类问题” 的目标。
而MVI在架构脚色中设计了Intent的脚色:


[*]它包含了业务调用的意图和数据
[*]从设计上可满足 调用 与 实现 的分离
[*]架构模型中以Intent流的形式出现,下游对其的 筛选 、转换 、 消费 等行为可遵照 FP范式 (即函数式编程范式、Functional Programming Patterns) ,逻辑的复用粒度为方法级,复用度更高更灵活
[*]解决了MVVM中的方向性问题、MVC/MVP 中的灵活度问题等
单一可信数据源

我猜测读者诸君都曾听过这个词,将 单一可信数据源 拆解一下:


[*]单一
[*]可信
[*]数据源
在MVI配景下,数据源 指的是视图对应的数据实体,它代表视图的内容状态。
可信指从数据源中获取的数据是 最新的、完备的、可靠的,否则是不可信的,我们没有理由在编码中使用不可信的数据源。
单一是指如许的数据源仅一个。
在经典设计中,其内涵如下图:
https://img-blog.csdnimg.cn/img_convert/a434fa5f1f9a2b407a02815886ab8b5a.webp?x-oss-process=image/format,png


[*]按照视图的 所有的 内容状态,界说一个不可变的 ViewState
[*]按照业务初始化 ViewState 实例
[*]Model业务天生驱动 ViewState变革的Result
[*]计算出新状态,Reduce(Pre-ViewState,Result) -> New-ViewState
[*]更新数据源
[*]View层消费ViewState
借助于数据绑定框架,可以很方便地解决视图更新的问题。
   想象一下,此时页面UI非常复杂……
假如僵化的信仰如许的 单一 ,情况会怎样呢?


[*]复杂(大量属性)的ViewState
[*]复杂的UI更新计算,e.g. 100个属性变了2个,依然必要计算98个属性未变大概全量逼迫更新
在 APP-A和APP-B中,我分别使用了 DataBinding和Compose,但均无法制止该问题。
作甚单一

从呆板实行步伐的原理上看,我们无法实现 多个内容一致的数据源 在 任意时刻 满足 最新的、可靠的。
将视图视为一个整体,规定它只拥有 一个 可信的数据源。在此基础上看局部的视图,它们也天真烂漫地仅拥有一个可信的数据源。
反过来看,当任意的局部视图仅具有一个可信数据源时,整体视图也仅有一个逻辑上的可信数据源。
据此,我们可以对 经典MVI实现 举行一定程度的改造,将ViewState举行局部分解,使得UI绑定部分的业务逻辑更 清晰、干净。
请留意,复杂度不会凭空消失,我们为了让 “UI绑定的业务逻辑更清晰、干净”、“更新UI的计算量更少”,将复杂度转移到了ViewState的拆分。拆分后,将具有 多个视图部件的单一可信数据源,留意,为了不引起额外的麻烦、并且便于维护扩展,发起遵守以下条件:


[*]基于业务需求,组合数据源形成新数据源
[*]不在数据源的逻辑范围之外举行数据源组合操作
   举个虚拟的例子:用户必要实名认证 且 关注博主 ,才在界面上体现某功能按钮。下面使用代码分别演示。
考虑到RxJava的广泛度仍旧高于Kotlin-Coroutine+flow,数据流的实现采用RxJava
留意,考虑到读者可能会编写demo做UDF局部的验证,下文中的代码以示例目标为主,分身编写场景冒烟的方便性,流的范例不一定是构建完备UDF的最佳选择。
经典实现

在经典MVI实现中,必要先界说ViewState
data class ViewState(
/unique id of current login user/
val userId: Int,
/true if the current login user has complete real-name verified/
val realNameVerified: Boolean,
/true if the current login user has followed the author/
val hasFollowAuthor: Boolean
) {
}
并界说ViewModel,创建ViewState流,忽略掉其初始化和其他部分
class VM {
val viewState = BehaviorSubject.create()
//ignore
}
并界说View层,忽略掉其他部分,简单起见临时不使用数据绑定框架
class View {
private val vm = VM()
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
vm.viewState.subscribe {
render(it)
}
}
private fun render(state: ViewState) {
imgRealNameVerified.isVisible = state.realNameVerified
cbHasFollowAuthor.isChecked = state.hasFollowAuthor
someButton.isVisible = state.realNameVerified && state.hasFollowAuthor
//ignore other
}
}
在JS中,JSON并不能附加逻辑,根本等价于Java中的POJO,故在数据源外部处理简单逻辑的情况较为常见。而在Java、Kotlin中可以举行适当的优化,适当封装,使得代码更加干净便于维护:
data class ViewState(
//ignore
) {
fun isSomeFuncEnabled():Boolean = realNameVerified && hasFollowAuthor
}
class View {
//ignore
private fun render(state: ViewState) {
//…
someButton.isVisible = state.isSomeFuncEnabled()
}
}
拆分实现

仍旧先界说逻辑上完备的ViewState:
class ComposedViewState(
/unique id of current login user/
val userId: Int,
) {
/**


[*]real-name-verified observable subject,feed true if the current login user has complete real-name verified
[*]*/
val realNameVerified = BehaviorSubject.create()
/**


[*]follow-author observable subject, feed true if the current login user has followed the author
[*]*/
val hasFollowAuthor = BehaviorSubject.create()
val someFuncEnabled = BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }
}
界说ViewModel,子模块数据流均已界说,故而无需再界说全ViewState的流
class VM(val userId: Int) {
val viewState = ComposedViewState(userId)
//ignore
}
编写View层的UI绑定,同样简单起见,不使用数据绑定框架
class View {
private val vm = VM(1)
lateinit var imgRealNameVerified: ImageView
lateinit var cbHasFollowAuthor: CheckBox
lateinit var someButton: Button
fun onCreate() {
//ignore view initialize
bindViewStateWithUI()
}
private fun bindViewStateWithUI() {
vm.viewState.realNameVerified.subscribe {
renderSection1(it)
}
vm.viewState.hasFollowAuthor.subscribe {
renderSection2(it)
}
vm.viewState.someFuncEnabled.subscribe {
renderSection3(it)
}
//…
}
private fun renderSection1(foo:Boolean) {
imgRealNameVerified.isVisible = foo
}
private fun renderSection2(foo:Boolean) {
cbHasFollowAuthor.isChecked = foo
}
private fun renderSection3(foo:Boolean) {
someButton.isVisible = foo
}
}
例子较为简单,在现实项目中,假如遇到复杂页面,则可以分块举行处理。
留意:现实情况中,并没有必要将每一个子数据源拆分到一个View级别的控件,那样过于啰嗦,例子因非常简单而无法丰满起来。 e.g. 针对每一块视图区,例如作者区域,界说子ViewState类,创建其数据流即可。
   作者按:务必评估,在一次Model业务产生的Result中,会引起数据流下游的更新次数。 为制止产生不可预期的问题,可通过类似以下方式,使下游响应次数体现和经典实现的情况一致。
额外界说PartialChange流大概功能等价的流,它用于标识 reduce 计算的开始和竣事,可以将此期间的数据流的变革延迟到最后发送终态
更加推荐界说功能上等价的流
class ComposedViewState(
/unique id of current login user/
val userId: Int,
) {
internal val changes = BehaviorSubject.create()
//ignore
val someFuncEnabled =
BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }.sync(PartialChange.Tag, changes)
}
inline fun <reified T, S> Observable.sync(tag: S, sync: BehaviorSubject): Observable {
return BehaviorSubject.combineLatest(this, sync) { source, syncItem ->
if (syncItem == tag) {
syncItem
} else {
source
}
}.filter { it is T }.cast(T::class.java)
}
修改PartialChange,为reduce函数添加边界:
PartialChange是Model产生的Result的体现物,封装了ViewState的reduce函数逻辑,即怎样从 Pre-ViewState 天生 新 ViewState
sealed class PartialChange {
open fun reduce(state: ComposedViewState) {
}
/**


[*]同步标志,从头开始到真实PartialChange之间,流的状态生效
[*]*/
object Tag : PartialChange()
object None : PartialChange()
class Foo(val a: Boolean, val b: Boolean) : PartialChange() {
override fun reduce(state: ComposedViewState) {
state.changes.onNext(Tag)
state.realNameVerified.onNext(a)
state.hasFollowAuthor.onNext(b)
state.changes.onNext(this)
}
}
}
要想优雅,必要工具

采用响应式流,制止命令式编码

想来这一点已不必要多做解释。
在Android中,存在 LiveData 组件,它通过简单的方式封装了可观测的数据,但实现方式简单也限制了它的功能 不敷强大 。因此,发起使用 RxJava 大概 Kotlin-Coroutine & flow 构建数据流。
本节便不再展开。
采用数据绑定框架

采用 jetpack-compose 大概 DataBinding 均可以移除枯燥的UI命令式逻辑,在APP-A中我使用了DataBinding,在APP-B中我使用了Compose。
在 ViewState的代码很棒时,均可以得到优秀的编程体验,从啰嗦的UI中解放出来。
   作者的个人观点:
关于Compose。Compose仍旧属于较新的事物,在商业项目中使用存在学习门槛和造轮工作。在目标用户具有较高容忍度的情况下,已然可以举行尝试。
关于DataBinding。一个近乎毁誉参半的工具,关于它的批判,大多会合于:xml中实现的逻辑难以阅读、维护,这现实上是对DataBinding设计的误解而带来的错误使用。
DataBinding本身具有天生VM层的功能,但这一功能并不敷够强大,且没有完善的使用引导,而在官方Demo中过分宣传了它,导致各人认为DataBinding就该如许使用。
仅使用基础的数据绑定功能、和Resource大概Context有关的功能(例如字符串模板)、组件生命周期绑定等,适度自界说绑定。
作甚状态、作甚事件。最后的一公里

首先区别于上文提到的UI事件,这里的状态和事件均产生于数据流的末段,而UI事件处于数据流的首段。
   UI事件属于:A possible action that the user can perform that is monitored by an application or the operating system (event listener). When an event occurs an event handler is called which performs a specific task
在展开之前,先用一张图回首总结上文中对于 单向数据流 & 单一可信数据源 的知识
https://img-blog.csdnimg.cn/img_convert/8e43e385ee6dceac5bd8470f73d7dd06.webp?x-oss-process=image/format,png
在 单向数据流动 章节中,提到了MVI的UDF设计:


[*]系统捕获的UI事件、其他侦听事件(例如熄屏、应用生命周期事件),天生Intent,压入Intent流中
[*]ViewModel层中筛选、转换、处理Intent,现实是使用Model层业务,产生业务效果,即PartialChange
[*]PartialChange经过Reducer计算处理得到最新的ViewState,压入ViewState流
https://img-blog.csdnimg.cn/img_convert/d405f041cfba1a2e2a9e57ccbd5ae5e5.png
https://img-blog.csdnimg.cn/img_convert/4f55c3a4e7081647848b57bbbb78d46e.png
网上学习资料一大堆,但假如学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技能提升。
必要这份系统化的资料的朋侪,可以戳这里获取
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都接待加入我们的的圈子(技能交流、学习资源、职场吐槽、大厂内推、口试辅导),让我们一起学习成长!
ocess=image/format,png)
在 单向数据流动 章节中,提到了MVI的UDF设计:


[*]系统捕获的UI事件、其他侦听事件(例如熄屏、应用生命周期事件),天生Intent,压入Intent流中
[*]ViewModel层中筛选、转换、处理Intent,现实是使用Model层业务,产生业务效果,即PartialChange
[*]PartialChange经过Reducer计算处理得到最新的ViewState,压入ViewState流
[外链图片转存中…(img-m9qEK9q6-1715588609607)]
[外链图片转存中…(img-j7QAsR4A-1715588609608)]
网上学习资料一大堆,但假如学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技能提升。
必要这份系统化的资料的朋侪,可以戳这里获取
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都接待加入我们的的圈子(技能交流、学习资源、职场吐槽、大厂内推、口试辅导),让我们一起学习成长!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 谈一谈在两个商业项目中使用MVI架构后的感悟_mvi viewstate拆分(4)