聊聊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 在此基础上进一步解决了 状态一致性数据流方向混乱 的问题,代价是更多的模板代码和更高的学习成本。

相关推荐
无忧智库3 小时前
智慧城管的范式革命:从“数字城管”到“城市大脑”的智能生态跃迁(PPT)
架构
掘根3 小时前
【微服务即时通讯】消息存储子服务1
微服务·架构·语音识别
Are_You_Okkk_3 小时前
研发运维一体化:开源知识库落地案例与价值探析
运维·人工智能·架构·开源
JackieZhengChina3 小时前
BMAD-METHOD 筑梦架构:AI 驱动的开源敏捷开发方法
人工智能·架构·开源
剑飞的编程思维3 小时前
技术评审方法与流程全解析-如何做好技术评审
git·架构·个人开发·学习方法·技术美术·代码复审
爱吃萝卜的美羊羊4 小时前
ruoyi-cloud微服务跨服务调用实例接口
微服务·架构·ruoyi-cloud
Swift社区4 小时前
分布式能力不是功能,而是一种架构约束
分布式·架构
jerwey5 小时前
app-unavailable-in-region
架构
志摩凛5 小时前
范畴论——前端与计算机领域的“抽象工具箱”:该用则用,该弃则弃
算法·架构