Skip to content

当我们在谈MVP、MVVM、MVI的时候,我们到底在谈什么?

💡 前言

  • 当我们谈论 MVP、MVVM、MVI 时,
  • 我们在谈的是:
    • UI 层的架构模式(Presentation Layer Architecture Pattern)
    • 也就是「界面展示层的职责划分与数据流动方式」。
  • 这是一种解耦合的数据基础架构。

📦 背景:我们为什么需要这些模式?

  • 早期在开发 UI(无论是 Android、iOS、Web)时,常常出现这样的代码结构:

  • 所有逻辑都在一个“巨大的控制器”或“Activity”或“ViewController”里。

  • 这就导致了:

    • UI 代码和业务逻辑严重耦合;
    • 难以测试;
    • 难以复用;
    • 状态难以管理。
  • 于是,架构模式的目的是:

    • 让“UI层代码结构化”,
    • 让“数据流动清晰”,
    • 让“状态管理可控”。

MVC

  • 最传统的方式是使用MVC。
    • 因为在Android的View体系下,布局文件一般放在了xml中,而Activity/Fragment则作为Controller,去实现UI视图数据刷新的动作。但受限于xml布局文件的能力实在是有限,很多动作不得不放在Activity/Fragment中来实现,于是这个Controller在某种程度上,也承担了大量的View的任务。
    • 这个时候如果把数据的获取方式依旧放在Activity/Fragment中,不可避免的是这里的内容会非常多,维护也将变得很为难。于是Google官方推荐大家使用MVP的方式去替代MVC。

MVP

  • 一种用契约和接口来实现数据回调的结构
  • MVP就是通过ViewRender和IPresenter的方式去把数据生成的动作放在了Presenter中,由Presenter去访问更上一层的Service,获取到相应的Data,再通过ViewRender把这个数据返回到View中,View可以在自己实现的接口根据数据去给视图资源刷新UI。
  • 但是,这种方式在后期也暴露了它的缺点。网上对它的缺点讨论主要是围绕在Presenter依旧太大了,但我觉得网上说的那种Presenter承担的任务太重这个说法,并不是MVP的问题,这是因为数据控制层应该更加细分成为各个模块,比如Repository、UseCase等等,这个划分哪怕是去到MVVM也是要做这一块的工作的,这个需要开发者和开发团队按照具体的业务去划分详细的职责模块。
  • 我个人认为,MVP被大家逐渐淘汰的原因,是因为Presenter和View之间的依赖注入必须由开发者手动去注入生命周期,这就取决于各个团队对MVP的认知和封装了。
  • 很常见的一点就是,一些人以及一些团队对MVP的编写得过于简陋。
    • 他们在封装MVP架构过程中,只是实现了数据回调,只实现了Presenter向View返回数据这一个功能,而忽略了生命周期的绑定。
    • 这就导致,当一个View被销毁了,Presenter的请求还在继续,当Presenter向View返回数据的时候,View中的对象已经被销毁了。
    • 这个时候在Java项目中就会出现NPE的情况,即使是在Kotlin项目中,能够通过空安全去避免一些NPE的情况,但也还是会出现内存泄漏以及ANR的情况。
  • 另外,比较让人不适应的一点,就是它通过大量的接口去实现数据回调,这点是Java的魅力,但也是Java显得很笨重的原因,当要去找一个业务的具体实现的时候,这就变得很繁琐了。
  • 所以为了避免参差不齐的MVP封装情况,Google在Jetpack中向开发者推出了强制注入生命周期的ViewModel,而且使用方式也是极其简单的。

为了避免把这篇文章拉得太长,新开了一篇文章,简略介绍了一下MVP的开发模式

想详细了解这一个的用法,去这里

MVVM

  • 一种通过观察者模式实现数据回调的单一数据源结构
  • 核心结构(这个图是结合我的理解用ProcessOn画的,每个人理解可能会有不同的偏差,如果有不同看法,结合你的业务去理解会更好) ..mvvm-architecture-framework

ViewModel的创建方式很简单,按照官方的推荐,注入相应的生命即可以获取到相应的ViewModel,当然它这里还涉及到工厂模式,这部分详细参见ViewModelProvider.Factory:

  • 自己生命周期的
kotlin
private val viewModel: MyViewModel by lazy {
    ViewModelProvider(
        this,
        MyViewModelFactory()
    )[MyViewModel::class.java]
}
  • 父容器生命周期的
kotlin
private val parentViewModel: RouteViewModel? by lazy {
    parentFragment?.let {
        ViewModelProvider(
            it,
            MyViewModelFactory()
        )[RouteViewModel::class.java]
    }
}

private val activityViewModel: ActivityViewModel? by lazy {
    activity?.let {
        ViewModelProvider(
            it,
            MyViewModelFactory()
        )[ActivityViewModel::class.java]
    }
}

ViewModel的实际编写和LiveData的使用

kotlin
class MyViewModel(
    private val repository: Repository,
): ViewModel() {
    private val _data: MutableLiveData<String> = MutableLiveData()
    val data: LiveData<String> = _data
    
    private val _errorMessage: MutableLiveData<Throwable> = MutableLiveData()
    val errorMessage: LiveData<Throwable> = _errorMessage
    
    private val _isRefresh: MutableLiveData<Boolean> = MutableLiveData()
    val isRefresh: LiveData<Boolean> = _isRefresh
    
    fun fetchData() {
        _isRefresh.value = true
        viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> {
            _errorMessage.value = throwable
            _isRefresh.value = false
        }}) {
            // Image repository.fetchData() run in background thread to fetch data.
            // Warning: when you want to post something to livedata, you need to stay at UI Thread.
            _data.value = repository.fetchData()
            _isRefresh.value = false
        }
    }
}

class MyFragment: Fragment() {
    private val viewModel: MyViewModel by lazy {
        ViewModelProvider(this)[MyViewModel::class.java]
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.isRefresh.observe(this) {
            showHideSpinner(it)
        }
        
        viewModel.errorMessage.observe(this) {
            Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
        }
        
        viewModel.data.observe(this) {
            updateUI(it)
        }
        
        viewModel.fetchData()
    }
    
    private fun updateUI(data: String) {
        // updateUI
        println(data)
    }
}
  • MVVM与MVP的不同,就是MVVM并不一定要采用双向绑定,受限于DataBinding双向绑定的能力实在是太弱了,在使用MVVM的时候,我个人更加喜欢使用单一数据源,这也是Google最推荐的方式。结合着LiveData这一个拥有生命周期注入的监听工具,ViewModel中的数据可以很方便返回给到View。
  • 值得提及的一点是,我个人常用的单Activity多Fragment模式,使用parentFragment的生命周期去实现路由跳转是一个很好解决路由问题的方式。
  • 当然不可避免的一点,当我们使用LiveData的时候,按照以往既定的想法,每一个LiveData会对应一个状态。像上面的示例,它就存在了刷新中、数据请求成功、数据请求失败着3个状态。 这就会导致在ViewModel中,LiveData变得异常多,我们俗称为LiveData爆炸。

MVI

  • 一个对MVVM的稍许优化的新结构
  • 为了避免出现LiveData爆炸,在MVVM开发的实际过程,我们可以使用一个枚举的方式,在Kotlin的角度,这个叫密封类。通过枚举的方式,我们可以把一个请求可能面对的多种状态列举出来,在LiveData实际去监听的时候,再通过switch/when的方式去拿到对应的每一种状态,然后实现各种状态下的UI刷新,例如updateUI、ToastError等等。
  • 状态决策,由Repo层处理好具体的UIState,然后ViewModel层处理线程的具体调度,UI层拿到相应的UIState进行渲染响应。
kotlin
sealed class MyUIState {
    class Success(val data: String): MyUIState()
    class Failed(val throwable: Throwable): MyUIState()
    object Refreshing: MyUIState()
}

class MyViewModel(
    private val repository: Repository,
): ViewModel() {
    private val _uiState: MutableLiveData<MyUIState> = MutableLiveData()
    val uiState: LiveData<MyUIState> = _uiState

    fun fetchData() {
        _uiState.value = MyUIState.Refreshing
        viewModelScope.launch(Dispatchers.Main + CoroutineExceptionHandler { _, throwable -> {
            _uiState.value = MyUIState.Failed(it)
        }}) {
            // Image repository.fetchData() run in background thread to fetch data.
            // Warning: when you want to post something to livedata, you need to stay at UI Thread.
            _uiState.value = MyUIState.Success(repository.fetchData)
        }
    }
}

class MyFragment: Fragment() {
    private val viewModel: MyViewModel by lazy {
        ViewModelProvider(this)[MyViewModel::class.java]
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.uiState.observe(this) {
            when (it) {
                is MyUIState.Success -> {
                    showHideSpinner(false)
                  	// updateUI
                    updateUI(it.data)
                }

                is MyUIState.Failed -> {
                    showHideSpinner(false)
                    // ToastError
                    Toast.makeText(context, it.throwable.message, Toast.LENGTH_SHORT).show()
                }

                is MyUIState.Refreshing -> {
                    showHideSpinner(true)
                }
            }
        }
    }

    private fun updateUI(data: String) {
        // updateUI
        println(data)
    }
}

📈 图表对比

模式核心组成职责划分数据流动方向
MVP (Model–View–Presenter)Model、View、PresenterView 负责展示,Presenter 负责业务逻辑,Model 负责数据双向(Presenter 与 View 互相调用)
MVVM (Model–View–ViewModel)Model、View、ViewModelViewModel 暴露可观察的数据(LiveData、StateFlow...),View 只订阅单向绑定(数据变化自动驱动 UI)
MVI (Model–View–Intent)Model、View、Intent(或 State)一切交互都是事件,状态是不可变的,View 根据状态渲染单向数据流(更严格的单向)

🧐 对比分析

特性MVPMVVMMVI
数据流方向双向单向(View 观察 ViewModel)严格单向
状态管理分散在 Presenter 中ViewModel 管理可观察状态中心化状态容器(immutable)
可测试性较好更好(因无 UI 依赖)最好(纯函数式状态流)
复杂度中等中等偏高高(但可维护性好)
典型应用Android 早期(MVP)Android Jetpack(MVVM)Jetpack Compose、React、Flutter(MVI)

补充说明

一般来说,MVVM最好是要分为5层。

  • View: 这个是命令式UI下的视图文件以及Activity/Fragment视图容器文件,在这里通过UIState实现数据的刷新。
  • ViewModel:这个是用于线程调度、将UIState通知到LiveData(或者Flow),让View能够正常监听到UIState的数据。
  • Repository:拼装DataModel成为UIState的一层。
    • 这一层是最有意思的一层,它需要点离散数学知识(比如合理的集合映射等等)才能把数据正确拼装起来,其他的层大多都是模板代码
    • 其他层的内容,大多都是模版代码,实际上可以考虑使用AOP切面编程 aspectj 将这一块的内容,封装好然后直接调用的。
  • UseCase:数据的不同来源,可能是本地数据,也可能是网络数据。
  • Service:数据的来源。

当然,如果数据来源只有一个,那么在开发中是可以为了便于开发而舍弃UseCase这一层的,这个要具体问题具体分析。

随便写写的,喜欢就好。 使用VitePress构建