一同养成写作习气!这是我参加「日新计划 4 月更文应战」的第4天,点击检查活动详情。

前言

最近看到不少介绍MVI架构,即Model-View-Intent的文章,有人留言说Google炒冷饭或许为了凑KPI“发明”了MVI这么一个词。和后端的朋友描述了一下,他们听了第一印象也是和MVVM如同区别不大。可是凭印象Google应该还没有到需求这样来凑数。

MVI到底是不是凑数的?通过案例与MVVM进行比较

去看了一下官网,发现彻底没有说到MVI这个词。。可是引荐的架构图确实是更新了,用来演示MVI也确实很搭。

MVI到底是不是凑数的?通过案例与MVVM进行比较

(官网图)

想了想,决议总结一下自己的发现,和掘友们一同讨论学习。

事例共享

看过一些剖析MVI的文章,里面完成的办法各式各样,细节也不尽相同。乃至关于Model边界的划分也会不一样。

下面先共享一下在特定场景下我的MVVMMVI完成(不重要的细节会省略)。

场景

先预设一个场景,咱们的界面(View/Fragment)里有一个锅。首要任务便是完成一道菜的烹饪:

flowchart LR
开战 --> 热油 --> 加菜 --> 加调料 --> 出锅 

几个需求注意的点:

  • 初始状况:开战
  • 加入资料时:都是异步获取资料,再加入锅中
  • 结束状况:出锅

本文首要是比较MVVMMVI,这儿只共享这两种完成。

经典MVVM

为了加强比照,这儿的完成比较挨近Android Architecture Components刚发布时官网的的代码架构和片段:

MVI到底是不是凑数的?通过案例与MVVM进行比较

(其时的官网图)
// 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是否显示loadingRepository决议(是否有正在进行的数据读取)。
  • 关于调查的LiveData要做出何种操作,UI层的逻辑代码往往无法避免。

很久以前也听说过用状况机(state machine)办理UI界面,可是思路仍是约束在运用多个LiveData,运用时进行兼并。尽管状况更清晰了,可是关于代码的可维护性并没有明显的协助,乃至ViewModel里还多了些兼并LiveData以及状况办理的代码。代码形似还更杂乱了。后来发现了Redux式的思路,才有了下面这个版本的MVI完成。

MVI

下图是我对这个思路的了解:

  • 单一信息源
  • 单向/环形数据流
MVI到底是不是凑数的?通过案例与MVVM进行比较

定义几个下面代码会用到的称号(不必细究命名,只需自己和团队觉得有意义叫什么都行):

  • 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

长处:

  • 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到底是不是凑数的?通过案例与MVVM进行比较

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

MVI到底是不是凑数的?通过案例与MVVM进行比较

题外话

从官网架构图的改动发生的联想:

MVI到底是不是凑数的?通过案例与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)。增加这么一层确实能够协助咱们解耦部分逻辑。