关于mvvm:MVI到底是不是凑数的通过案例与MVVM进行比较

52次阅读

共计 6590 个字符,预计需要花费 17 分钟才能阅读完成。

前言

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

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

(官网图)

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

案例分享

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

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

场景

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

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

几个须要留神的点:

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

本文次要是比拟 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 的逻辑会在这里
    }
    ...
}

特点:

  • 应用多个 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.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 只是为了唬人,让人一听到就晓得你使用了 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)。增加这么一层确实能够帮忙咱们解耦局部逻辑。

正文完
 0