聊聊MVVM与MVI

25 阅读3分钟

MVVM vs MVI 架构对比

一、核心概念

MVVM(Model-View-ViewModel)

┌───────────┐     观察/绑定      ┌─────────────┐    请求数据    ┌───────────┐
│           │ ◄──────────────── │             │ ────────────► │           │
│   View    │                   │  ViewModel  │               │   Model   │
│           │ ────事件调用────►  │             │ ◄──返回数据── │           │
└───────────┘                   └─────────────┘               └───────────┘
  • View:UI 层,观察 ViewModel 的数据变化
  • ViewModel:持有 UI 状态(多个 LiveData/StateFlow),处理业务逻辑
  • Model:数据源(Repository、网络、数据库等)

MVI(Model-View-Intent)

┌───────────┐                   ┌─────────────┐               ┌───────────┐
│           │ ──── Intent ────► │             │ ────请求────► │           │
│   View    │                   │  ViewModel  │               │   Model   │
│           │ ◄── State ─────  │  (Reducer)  │ ◄──数据────── │           │
└───────────┘                   └─────────────┘               └───────────┘
      ▲                                │
      └────────── 单一状态流 ───────────┘

        Intent → Reducer → NewState → Render(单向数据流)
  • Intent:用户意图(事件),用密封类统一描述
  • Model:这里特指不可变的 UI State(整个页面的状态快照)
  • View:根据唯一 State 渲染 UI

二、代码对比(Android / Kotlin)

MVVM 典型实现

// ── ViewModel ──
class UserViewModel(private val repo: UserRepository) : ViewModel() {

    // ⚠️ 多个独立的状态流
    private val _userName = MutableStateFlow("")
    val userName: StateFlow<String> = _userName

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading

    private val _error = MutableSharedFlow<String>()
    val error: SharedFlow<String> = _error

    fun loadUser(id: String) {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val user = repo.getUser(id)
                _userName.value = user.name
            } catch (e: Exception) {
                _error.emit(e.message ?: "Unknown error")
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun updateName(name: String) {
        _userName.value = name
    }
}

// ── View (Activity/Fragment) ──
class UserFragment : Fragment() {
    private val vm: UserViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // 分别观察多个状态
        lifecycleScope.launch { vm.userName.collect { tvName.text = it } }
        lifecycleScope.launch { vm.isLoading.collect { progressBar.isVisible = it } }
        lifecycleScope.launch { vm.error.collect { showToast(it) } }

        btnLoad.setOnClickListener { vm.loadUser("123") }
    }
}

MVI 典型实现

// ── 契约:统一定义 Intent、State、Effect ──
// Intent(用户意图)
sealed class UserIntent {
    data class LoadUser(val id: String) : UserIntent()
    data class UpdateName(val name: String) : UserIntent()
}

// State(不可变的唯一 UI 状态)
data class UserState(
    val userName: String = "",
    val isLoading: Boolean = false,
)

// Side Effect(一次性事件)
sealed class UserEffect {
    data class ShowError(val message: String) : UserEffect()
}

// ── ViewModel ──
class UserViewModel(private val repo: UserRepository) : ViewModel() {

    // ✅ 唯一状态源
    private val _state = MutableStateFlow(UserState())
    val state: StateFlow<UserState> = _state

    private val _effect = Channel<UserEffect>()
    val effect: Flow<UserEffect> = _effect.receiveAsFlow()

    // ✅ 统一入口处理 Intent
    fun handleIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUser -> loadUser(intent.id)
            is UserIntent.UpdateName -> reduce { copy(userName = intent.name) }
        }
    }

    private fun loadUser(id: String) {
        viewModelScope.launch {
            reduce { copy(isLoading = true) }
            try {
                val user = repo.getUser(id)
                reduce { copy(userName = user.name, isLoading = false) }
            } catch (e: Exception) {
                reduce { copy(isLoading = false) }
                _effect.send(UserEffect.ShowError(e.message ?: "Unknown"))
            }
        }
    }

    // 线程安全的状态更新
    private fun reduce(block: UserState.() -> UserState) {
        _state.update { it.block() }
    }
}

// ── View ──
class UserFragment : Fragment() {
    private val vm: UserViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // ✅ 只需观察一个状态
        lifecycleScope.launch {
            vm.state.collect { state ->
                tvName.text = state.userName
                progressBar.isVisible = state.isLoading
            }
        }
        // 一次性事件
        lifecycleScope.launch {
            vm.effect.collect { effect ->
                when (effect) {
                    is UserEffect.ShowError -> showToast(effect.message)
                }
            }
        }

        btnLoad.setOnClickListener {
            vm.handleIntent(UserIntent.LoadUser("123"))
        }
    }
}

三、关键差异总结

维度MVVMMVI
数据流向双向 / 部分单向严格单向(Intent → State → UI)
状态管理多个分散的 LiveData/StateFlow单一不可变 State(Single Source of Truth)
用户事件View 直接调用 ViewModel 方法通过 sealed class Intent 统一派发
状态一致性⚠️ 多状态可能不同步✅ 原子更新,不会出现状态不一致
可追踪/可调试较难(状态分散)✅ 每次状态变更有迹可循(类似 Redux)
模板代码较少较多(需定义 Intent、State、Effect)
学习曲线较低较高
适合场景中小页面、状态简单复杂页面、多状态交互、需要时间旅行调试

四、状态一致性问题示例

这是 MVVM 最常见的痛点:

// MVVM 中的潜在问题
_isLoading.value = false
// ⬇ 此处如果发生线程切换,UI 可能在 isLoading=false 但 data 还没更新时渲染
_data.value = newData

// MVI 中不存在此问题 —— 原子更新
reduce { copy(isLoading = false, data = newData) }

五、架构演进关系

MVC  ──►  MVP  ──►  MVVM  ──►  MVI
                      │          │
               多个可观察状态   单一不可变状态
               方法调用        Intent 驱动
                              单向数据流

MVI 可以看作 MVVM + 单向数据流 + 状态集中管理 的增强版。


六、选型建议

项目复杂度低 / 快速开发        ──► MVVM(够用且高效)
                                  
页面状态多且相互关联           ──► MVI(状态一致性好)
需要精确的状态追踪与调试       ──► MVI
团队已有 Redux/Flux 经验       ──► MVI(思想相通)

实际工程中:可以混合使用
  • 简单页面用 MVVM
  • 复杂业务流用 MVI

一句话总结:

MVVM 解决了 View 与 Model 的解耦问题;MVI 在此基础上进一步解决了 状态一致性数据流方向混乱 的问题,代价是更多的模板代码和更高的学习成本。