Android开发经验谈

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

2022-04-26  本文已影响0人  刨坑

前言

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

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

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

案例分享

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

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

场景

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

几个需要注意的点:

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

经典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的逻辑会在这里
    }
    ...
}z

特点:

很久以前也听说过用状态机(state machine)管理UI界面,但是思路还是限制在使用多个LiveData,使用时进行合并。虽然状态更清晰了,但是对于代码的可维护性并没有明显的帮助,甚至ViewModel里还多了些合并LiveData以及状态管理的代码。代码貌似还更复杂了。后来发现了Redux式的思路,才有了下面这个版本的MVI实现。

MVI

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

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

下面开始展示代码:

// 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)
    ...
}

特点:

分析

经典MVVM

优点:

缺点:

MVI

优点:

缺点:

比较

两种架构都有优缺点。

因为大家都熟悉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)。添加这么一层的确可以帮助我们解耦部分逻辑。

上一篇 下一篇

猜你喜欢

热点阅读