聊聊MVVM与MVI

MVVM vs MVI 架构对比

一、核心概念

MVVM(Model-View-ViewModel)

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

MVI(Model-View-Intent)

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

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

二、代码对比(Android / Kotlin)

MVVM 典型实现

kotlin 复制代码
// ── 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 典型实现

kotlin 复制代码
// ── 契约:统一定义 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"))
        }
    }
}

三、关键差异总结

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

四、状态一致性问题示例

这是 MVVM 最常见的痛点:

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

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

五、架构演进关系

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

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


六、选型建议

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

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

一句话总结:

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

相关推荐
乐维_lwops4 小时前
五层架构全景解析:Lerwee 运维智能体如何实现 “从感知到行动”(二)
运维·架构·运维智能体
TechMasterPlus4 小时前
LangGraph 实战指南:构建状态驱动的 LLM 应用架构
人工智能·架构
LT10157974445 小时前
2026 年自动化测试工具对比:架构与场景深度评测
测试工具·架构·自动化
努力搬砖的咸鱼7 小时前
Label 与 Selector:Kubernetes 资源选择的核心机制
微服务·云原生·容器·架构·kubernetes
CoovallyAIHub8 小时前
无人机拍叶片→AI找缺陷:CEA-DETR改进RT-DETR做风电叶片表面缺陷检测,mAP50达89.4%
算法·架构·github
CoovallyAIHub8 小时前
混合训练反而更差?VLM Agent在训练前协调跨数据集标注,文档布局检测F-score从0.860提升至0.883
算法·架构·github
文心快码BaiduComate9 小时前
里程碑突破 | 文心快码中标国家开发银行代码研发助手项目
前端·后端·架构
Duang9 小时前
AI 真能自己写出整个 Windows 系统吗?我做了一场无监督实验
算法·设计模式·架构
淘源码d10 小时前
一套成熟的智慧工地平台开发方案
架构·源码·智慧工地·开发方案
gyx_这个杀手不太冷静10 小时前
大人工智能时代下前端界面全新开发模式的思考(四)
前端·架构·ai编程