日期:
来源:刘望舒收集编辑:点击蓝字关注☞
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:没错!皇叔开了个训练营
作者:leobert-lan
https://juejin.cn/user/2066737589654327/posts
前言
MVI并非新兴事物,在2020年时亦曾有通过撰写一篇文章与诸位读者探讨一二的念头。
当时项目采用MVP分层设计,组员的代码风格差异也较大,代码中类职责赋予与封装风格各成一套,随着业务急速膨胀,代码越发混乱。试图用 MVI架构 + 单向流 形成 掣肘 带来一致风格。但这种做法不够以人为本,最终采用 "在MVP的基础上进行了适当改造+设计约定的方式" 解决了问题,并未将MVI投入到商业项目中,于是 放弃了纸上谈兵。
在半年前终于有机会在商业项目中进行实践,同诸位谈一谈使用后的 个人感悟 ,并藉此讲透MVI等架构。
所有内容将按照以下要点展开:
从架构的理念出发 -- 简单列明各种 MVX 的理念 , MVX:指代 MVC、MVP、MVVM、MVI 拥抱复杂的同时实现简化 -- 通过对比理解单向数据流动所解决的痛点、设计Intent的原因等问题 单一可信数据源,不可僵化信奉 要想优雅,需要工具 -- 借助声明式、响应式编程工具,构建流,屏蔽命令式编程中的细节,同样是聚焦和简化 状态和事件分家,绝不是吃饱了撑的 -- 为什么要裂变出状态和事件,如何界定
APP-A 是Android项目,图方便纯kotlin APP-B 是 Compose-Desktop项目,不得不kotlin
从架构出发
Intent:驱动model发生改变的意图,以UI中的事件最为常见; Model:业务模型,包含数据和逻辑,是对应 客观实体 的 程序建模; View:表现层的视图,以UI方式呈现Model的状态(以及事件),接受用户输入,转换为UI事件
MVC、MVP
MVVM
MVI
将双向绑定退化为单向绑定,View层消费UI状态流和事件流,这也意味着UI状态的职责精简,它不再承载View层的用户输入等事件 将UI状态独立,Model层仅产生 UI状态的局部变化 和 事件
单向数据流动
单向 是指 单一方向 此处的 数据 是广义的、宽泛的。 仅描述数据流的 变化方向 ,与数据流的数量无关,但一般 形成有效工作 均需要两条数据流(上行数据流和下行数据流)
Model层业务结果使其变化,并期望它驱动UI更新 View层发生事件,反馈数据变化,并期望它驱动Model层逻辑
MVC/MVP中,View层通过调用C/P层API的方式最终调用到Model层业务,方式质朴、无难度。但业务量规模增大后接口方法数也会增多,导致C/P层尾大不掉,难以重用。 MVVM中,VM层总是需要利用 技巧 进行模型概念转换,以满足业务响应满足实际需求,需要很深厚的设计经验才能写出非常优秀的代码,这并不友好。
它包含了业务调用的意图和数据 从设计上可满足 调用 与 实现 的分离 架构模型中以Intent流的形式出现,下游对其的 筛选 、转换 、 消费 等行为可遵循 FP范式 (即函数式编程范式、Functional Programming Patterns) ,逻辑的复用粒度为方法级,复用度更高更灵活 解决了MVVM中的方向性问题、MVC/MVP 中的灵活度问题等
单一可信数据源
单一 可信 数据源
按照视图的 所有的 内容状态,定义一个不可变的 ViewState 按照业务初始化 ViewState 实例 Model业务生成驱动 ViewState变化的Result 计算出新状态,Reduce(Pre-ViewState,Result) -> New-ViewState 更新数据源 View层消费ViewState
复杂(大量属性)的ViewState 复杂的UI更新计算,e.g. 100个属性变了2个,依然需要计算98个属性未变或者全量强制更新
基于业务需求,组合数据源形成新数据源 不在数据源的逻辑范围之外进行数据源组合操作
经典实现
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
) {
}
class VM {
val viewState = BehaviorSubject.create<ViewState>()
//ignore
}
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()
}
}
拆分实现
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<Boolean>()
/**
* follow-author observable subject, feed true if the current login user has followed the author
* */
val hasFollowAuthor = BehaviorSubject.create<Boolean>()
val someFuncEnabled = BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }
}
class VM(val userId: Int) {
val viewState = ComposedViewState(userId)
//ignore
}
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
}
}
class ComposedViewState(
/*unique id of current login user*/
val userId: Int,
) {
internal val changes = BehaviorSubject.create<PartialChange>()
//ignore
val someFuncEnabled =
BehaviorSubject.combineLatest(realNameVerified, hasFollowAuthor) { a, b -> a && b }.sync(PartialChange.Tag, changes)
}
inline fun <reified T, S> Observable<T>.sync(tag: S, sync: BehaviorSubject<S>): Observable<T> {
return BehaviorSubject.combineLatest(this, sync) { source, syncItem ->
if (syncItem == tag) {
syncItem
} else {
source
}
}.filter { it is T }.cast(T::class.java)
}
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)
}
}
}
优雅,工具
状态,事件
系统捕获的UI事件、其他侦听事件(例如熄屏、应用生命周期事件),生成Intent,压入Intent流中 ViewModel层中筛选、转换、处理Intent,实际是使用Model层业务,产生业务结果,即PartialChange PartialChange经过Reducer计算处理得到最新的ViewState,压入ViewState流 View层(广义的表现层)响应并呈现最新的ViewState
Event 体现的是特定的变化 State 体现的是客观实体在任意时刻都适用的一组情况,即一段时间内无变化的条件或者特征
应用需要弹出一个气泡通知用户这一事件 应用需要更新消息数,消息列表内容等,以呈现出最新的State
State 只需要考虑呈现的准确性和及时性,除去美观、可理解性等等 Event 需要考虑准确性、优先级、及时性、按条件丢弃等等,除去美观、可理解性等等
可能一瞬间的断开网络连接,会导致多个连接均返回失败 可能连接问题未修复,10秒前请求失败,当前请求又失败了
发生时间 客观有效期 判断有效的条件(如呈现的条件) 判断失效的条件 ,用于实现提前失效
State之间无生命周期重叠 所有State的生命周期相加可填满时间轴
Event可能存在生命周期重叠 所有Event的生命周期相加可能无法覆盖完整的时间轴
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号