前言
最近看到不少介绍 MVI
架构,即 Model-View-Intent
的文章,有人留言说 Google 炒冷饭或者为了凑 KPI“创造”了 MVI
这么一个词。和后端的敌人形容了一下,他们听了第一印象也是和 MVVM
如同区别不大。然而凭印象 Google 应该还没有到须要这样来凑数。
去看了一下官网,发现齐全没有提到 MVI
这个词。。然而举荐的架构图的确是更新了,用来演示 MVI
也的确很搭。
(官网图)
想了想,决定总结一下本人的发现,和掘友们一起探讨学习。
案例分享
看过一些剖析 MVI
的文章,外面实现的办法各种各样,细节也不尽相同。甚至对于 Model
边界的划分也会不一样。
上面先分享一下在特定场景下我的 MVVM
和MVI
实现(不重要的细节会省略)。
场景
先预设一个场景,咱们的界面(View/Fragment
)里有一个锅。次要工作就是实现一道菜的烹饪:
flowchart LR
停火 --> 热油 --> 加菜 --> 加调料 --> 出锅
几个须要留神的点:
- 初始状态:停火
- 退出资料时:都是异步获取资料,再退出锅中
- 完结状态:出锅
本文次要是比拟 MVVM
和MVI
,这里只分享这两种实现。
经典 MVVM
为了增强比照,这里的实现比拟靠近 Android Architecture Components
刚公布时官网的的代码架构和片段:
(过后的官网图)
// PotFragment.kt
class PotFragment {
...
// 察看是否点火
viewModel.fireStatus.observe(
viewLifecycleOwner,
Observer {updateUi()
if (fireOn) addOil()}
)
// 察看油温
viewModel.oilTemp.observe(
viewLifecycleOwner,
Observer {updateUi()
if (oilHot) addIngredients()}
)
// 察看菜熟没熟
viewModel.ingredientsStatus.observe(
viewLifecycleOwner,
Observer {updateUi()
if (ingredientsCooked) {
// 加调料
addPowder(SALT)
addPowder(SOY_SAUCE)
}
}
)
// 察看油盐是否加完
viewModel.allPowderAdded.observe(
viewLifecycleOwner,
Observer {// 出锅!}
)
viewModel.loading.observe(
viewLifecycleOwner,
Observer {if (loading) {// 颠勺} else {// 放下锅}
}
)
// 所有准备就绪,点火
turnOnFire()
...
}
// PotViewModel.kt
class PotViewModel(val repo: CookingRepository) {private val _fireStatus = MutableLiveData<FireStatus>()
val fireStatus: LiveData<FireStatus> = _fireStatus
private val _oilTemp = MutableLiveData<OilTemp>()
val oilTemp: LiveData<OilTemp> = _oilTemp
private val _ingredientsStatus = MutableLiveData<IngredientsStatus>()
val ingredientsStatus: LiveData<IngredientsStatus> = _ingredientsStatus
// 所有调料加好了才更新。这里 Event 外部会有 flag 提醒这个 LiveData 的更新是否被应用过
//(当年咱们还真用这种形式实现过单次生产的 LiveData)。private val _allPowderAdded = MutableLiveData<Event<Boolean>>()
val allPowderAdded: LiveData<Event<Boolean>> = _allPowderAdded
// 假如曾经实现逻辑从 repo 获取是否有还在进行的数据获取
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun turnOfFire() {}
// 假如上面都是异步获取资料,这里简化一下代码
fun addOil() {repo.fetchOil()
}
fun addIngredients() {repo.fetchIngredients()
}
fun addPowder(val powderType: PowderType) {repo.fetchPowder(powderType)
// 更新_allPowderAdded 的逻辑会在这里
}
...
}
特点:
- 应用多个
LiveData
察看不同的数据,并以此来更新UI
。每个LiveData
都是一个State
,每个View
有本人的State
。 UI
是否显示loading
由Repository
决定(是否有正在进行的数据读取)。- 对于察看的
LiveData
要做出何种操作,UI
层的逻辑代码往往无奈防止。
很久以前也据说过用状态机(state machine)治理 UI
界面,然而思路还是限度在应用多个 LiveData
,应用时进行合并。尽管状态更清晰了,然而对于代码的可维护性并没有显著的帮忙,甚至ViewModel
里还多了些合并 LiveData
以及状态治理的代码。代码貌似还更简单了 。起初发现了Redux
式的思路,才有了上面这个版本的 MVI
实现。
MVI
下图是我对这个思路的了解:
- 繁多信息源
- 单向 / 环形数据流
定义几个上面代码会用到的名称(不必细究命名,只有本人和团队感觉有意义叫什么都行):
- State:不论数据从哪里来,通过什么解决,都会归于当初的 状态。
- Event:上图中的 用意 产生或代表的事件,也能够了解为
Intent
或者Action
,最终产生Event
让咱们更新State
。 - Reducer:驱动 状态变动的外围。这个例子里能够设想成厨师的手,用来扭转锅的状态。
- Side effects:用户无感知,就当它是“额定成果”(或者“副作用”)。对于数据的申请或者记录上传用户操作的代码都归于此类。
上面开始展现~ 伪~ 代码:
// PotState.kt
sealed class PotState {object Initial: CookingStatus()
object FireOn: CookingStatus()
class Cooking(val data: List<EdibleStuff>): CookingStatus()
object Finished: CookingStatus()}
// CookEvent.kt
sealed class CookEvent {object TurnOnFire(): CookEvent()
object RequestOil(): CookEvent()
object AddOil(): CookEvent()
class RequestIngredient(val ingredientType: IngredientType): CookEvent()
class AddIngredient(val ingredient: Ingredient): CookEvent()
class RequestPowder(val powderType: PowderType): CookEvent()
class AddPowder(val powder: Powder): CookEvent()
object ServeFood()}
// models.kt
interface EdibleStuff
data class Oil(...) implements EdibleStuff
data class Ingredient(...) implements EdibleStuff
data class Powder(...) implements EdibleStuff
// PotReducer.kt
class PotReducer {fun reduce(state: PotState, event: CookEvent) =
when (state) {Initial -> reduceInitial(event)
FireOn -> reduceFireOn(event)
is Cooking -> reduceCooking(event)
Finished -> reduceFinished(state, event)
}
// 每个状态只承受某些特定的 Event,其它的会疏忽(无奈影响以后状态)private fun reduceInitial(state: PotState, event: CookEvent) =
when (event) {TurnOnFire -> flowOf(FireOn) // 生成一个 Cooking 状态并加好油
else -> // handle exception
}
private fun reduceFireOn(state: PotState, event: CookEvent) =
when (event) {AddOil -> flowOf(Cooking(mutableListOf<Cooking>(Oil)) // 生成一个 Cooking 状态并加好油
else -> // handle exception
}
private fun reduceCooking(state: PotState, event: CookEvent) =
when (event) {AddIngredient -> flowOf(state.apply { data.add(event.ingredient) }) // 加菜
AddPowder -> flowOf(state.apply { data.add(event.powder) }) // 加调料
else -> // handle exception
}
private fun reduceFinished(state: PotState, event: CookEvent) =
when (event) {ServeFood -> flowOf(Finished) // 出锅
else -> // handle exception
}
}
// PotViewModel.kt
class PotViewModel(val potReducer: PotReducer, val repo: CookingRepository) {
...
var potState: PotState = Initial
// 生成下一状态,更新 Flow
fun processEvent(event: CookEvent) =
potReducer.reduce(potState, event)
.updateState()
.handleSideEffects(event)
.launchIn(viewModelScope)
// 对于不间接影响 UI 的事件,当做 side effects 解决
private fun handleSideEffects(event: CookEvent) =
onEach { event ->
when (event) {is RequestOil -> fetchOil()
is RequestIngredient -> fetchIngredient(...)
is RequestPowder -> fetchPowder(...)
}
}
// 收到 Repository 传来的食料,启动新 Event:增加入锅
private fun fetchOil() = repo.fetchOil().onEach {processEvent(AddOil) }.collect()
// fetchIngredient(...) 与 fetchPowder(...) 也相似
...
}
// PotFragment.kt
class PotFragment {
...
@Composable
fun Pot(viewModel: PotViewModel) {val state by viewModel.potState.collectAsState()
Column {
//Render toolbar
Toolbar(...)
//Render screen content
when (state) {
FireOn -> // render UI
is Cooking -> // render UI
Finished -> // render UI:出锅!
}
}
}
// 准备就绪,挑个适合的机会停火
viewModel.processEvent(TurnOnFire)
...
}
特点:
- Fragment/Activity 只负责渲染
- 用户用意会产生 Event,并被 ViewModel 中的 Reducer 解决
- 特定的状态下,只会接管能被解决的 Event
剖析
经典 MVVM
长处:
- 相比
MVC
或者MVP
,置信大家都相熟。
毛病:
- 每个
View
有本人的State
。很多View
混合在一起时,代码和咱们的思路都容易变凌乱。审核代码也须要对全局有很好的了解。 - 须要察看的数据多了之后,
LiveData
治理能够变得很简单。 - 能够看到,
Fragment
中无论何时都在察看并接管所有LiveData
的更新。认真想想,其实这当中是蕴含了一些逻辑的。比如说,停火之后咱们不心愿接管加调料的操作。这些逻辑不容易独自拿进去写测试,通常要被蕴含在 Fragment 的测试离。
MVI
长处:
State
是single source of truth
,繁多信息源,不必放心各个View
的状态到处都是,甚至互相抵触。- 随同着预设的状态值,能够承受的用意
Intent
或者操作Action
也能够预设。不在打算里的用意 / 操作不会对 UI 界面产生影响,也不会有额定成果。审核代码只须要理解新增的用意对某一两个受影响的状态就足够,不必把整个界面的内容都复盘一遍。单元测试也是相似。也算是合乎 关注点拆散(Separation of Concerns)。
毛病:
- 随着 View 变得复杂,能够有的状态以及能承受的用意也会迅速收缩。
- 文件数量变多(这个和从 MVC 到 MVP 的感觉有点像)。
- 老手学习、了解起来不容易。
比拟
两种架构都有优缺点。
因为大家都相熟MVVM
,新团队的接受度必定会好。
有些毛病也能够想方法改良。例如 MVI
的状态收缩能够通过划分为几个小的分状态来缓解。
对于简单的场景,我集体更偏向于采纳 MVI
的全局状态治理 的思路。次要还是感觉传统 MVVM
每次增加新的 LiveData
时(当然当初经常用 Flow
),须要仔细检查其它所有的View
或者LiveData
,惟恐漏掉什么改变,不利于高效开发和保护。
总结
我认为传统的 MVVM
和MVI
次要的区别还是在于全局状态治理。而且这个全局状态治理的思路用传统 MVVM
架构也能实现,很多人感觉 MVI
和MVVM
差不多的起因可能正是如此。 其实也难能可贵,不少设计模式两两之间也很类似,但并不障碍大家给他们安上不同的名字。只有咱们把握住外围概念,正当使用,叫什么名字也不重要。正如官网的倡议:
就算叫 MVI
只是为了唬人,让人一听到就晓得你使用了 Redux/State machine
的思路,而不是“经典”的安卓版MVVM
,如同也是个不错的理由。
题外话
从官网架构图的变动产生的联想:
ViewModel 化身 LifecycleObserver
最近看到不少文章分享他们对于让 ViewModel
也lifecycle-aware
的试验。从官网文档看,UI elements
和 State holders
(在我看来就是Fragment/Activity
和ViewModel
)也在被视作一个整体的UI Layer
。不晓得当前是不是会有这么一个趋势。
有时候,不经意间就会错过一些乏味实用的想法。回忆 2017 年的时候,听到 WeWork
的员工分享他们自制的 Declarative UI
库。过后感觉都不能预览,应该不会好用到哪去吧。没想到起初官网公布了Compose
,预览性能都退出了Android Studio
。
选择性应用的 Domain Layer
兴许是随着这几年 Clean Architecture
的热度回升,看到不少团队开始退出畛域层。官网举荐的架构图(结尾提到)中也退出了Domain Layer (optional)
。增加这么一层确实能够帮忙咱们解耦局部逻辑。