前言
最近看到不少介绍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.ktclass 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.ktclass 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.ktsealed class PotState { object Initial: CookingStatus() object FireOn: CookingStatus() class Cooking(val data: List<EdibleStuff>): CookingStatus() object Finished: CookingStatus()}// CookEvent.ktsealed 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.ktinterface EdibleStuffdata class Oil(...) implements EdibleStuffdata class Ingredient(...) implements EdibleStuffdata class Powder(...) implements EdibleStuff// PotReducer.ktclass 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.ktclass 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.ktclass 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)
。增加这么一层确实能够帮忙咱们解耦局部逻辑。