前言

最近看到不少介绍MVI架构,即Model-View-Intent的文章,有人留言说Google炒冷饭或者为了凑KPI“创造”了MVI这么一个词。和后端的敌人形容了一下,他们听了第一印象也是和MVVM如同区别不大。然而凭印象Google应该还没有到须要这样来凑数。

去看了一下官网,发现齐全没有提到MVI这个词。。然而举荐的架构图的确是更新了,用来演示MVI也的确很搭。

(官网图)

想了想,决定总结一下本人的发现,和掘友们一起探讨学习。

案例分享

看过一些剖析MVI的文章,外面实现的办法各种各样,细节也不尽相同。甚至对于Model边界的划分也会不一样。

上面先分享一下在特定场景下我的MVVMMVI实现(不重要的细节会省略)。

场景

先预设一个场景,咱们的界面(View/Fragment)里有一个锅。次要工作就是实现一道菜的烹饪:

flowchart LR停火 --> 热油 --> 加菜 --> 加调料 --> 出锅 

几个须要留神的点:

  • 初始状态:停火
  • 退出资料时:都是异步获取资料,再退出锅中
  • 完结状态:出锅

本文次要是比拟MVVMMVI,这里只分享这两种实现。

经典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是否显示loadingRepository决定(是否有正在进行的数据读取)。
  • 对于察看的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

长处:

  • Statesingle source of truth,繁多信息源,不必放心各个View的状态到处都是,甚至互相抵触。
  • 随同着预设的状态值,能够承受的用意Intent或者操作Action也能够预设。不在打算里的用意/操作不会对UI界面产生影响,也不会有额定成果。审核代码只须要理解新增的用意对某一两个受影响的状态就足够,不必把整个界面的内容都复盘一遍。单元测试也是相似。也算是合乎关注点拆散(Separation of Concerns)。

毛病:

  • 随着View变得复杂,能够有的状态以及能承受的用意也会迅速收缩。
  • 文件数量变多(这个和从MVC到MVP的感觉有点像)。
  • 老手学习、了解起来不容易。

比拟

两种架构都有优缺点。

因为大家都相熟MVVM,新团队的接受度必定会好。

有些毛病也能够想方法改良。例如MVI的状态收缩能够通过划分为几个小的分状态来缓解。

对于简单的场景,我集体更偏向于采纳MVI全局状态治理的思路。次要还是感觉传统MVVM每次增加新的LiveData时(当然当初经常用Flow),须要仔细检查其它所有的View或者LiveData,惟恐漏掉什么改变,不利于高效开发和保护。

总结

我认为传统的MVVMMVI次要的区别还是在于全局状态治理。而且这个全局状态治理的思路用传统MVVM架构也能实现,很多人感觉MVIMVVM差不多的起因可能正是如此。 其实也难能可贵,不少设计模式两两之间也很类似,但并不障碍大家给他们安上不同的名字。只有咱们把握住外围概念,正当使用,叫什么名字也不重要。正如官网的倡议:

就算叫MVI只是为了唬人,让人一听到就晓得你使用了Redux/State machine的思路,而不是“经典”的安卓版MVVM,如同也是个不错的理由。

题外话

从官网架构图的变动产生的联想:

ViewModel 化身 LifecycleObserver

最近看到不少文章分享他们对于让ViewModellifecycle-aware的试验。从官网文档看,UI elementsState holders(在我看来就是Fragment/ActivityViewModel)也在被视作一个整体的UI Layer。不晓得当前是不是会有这么一个趋势。

有时候,不经意间就会错过一些乏味实用的想法。回忆2017年的时候,听到WeWork的员工分享他们自制的Declarative UI库。过后感觉都不能预览,应该不会好用到哪去吧。没想到起初官网公布了Compose,预览性能都退出了Android Studio

选择性应用的 Domain Layer

兴许是随着这几年Clean Architecture的热度回升,看到不少团队开始退出畛域层。官网举荐的架构图(结尾提到)中也退出了Domain Layer (optional)。增加这么一层确实能够帮忙咱们解耦局部逻辑。