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

相关推荐
phltxy4 小时前
微服务高可用实战:Sentinel 熔断与限流从入门到精通
微服务·架构·sentinel
Walter先生10 小时前
MCP行情数据接入配置踩坑全记录:从Claude Code到Zed八大客户端适配实战
后端·websocket·架构·实时行情数据源
ai产品老杨10 小时前
突破品牌壁垒:基于 GB28181 与 RTSP 的异构 AI 视频平台架构深度解析(支持 Docker 与源码交付)
人工智能·架构·音视频
AI服务老曹10 小时前
【架构深析】打破安防“黑盒”:GB28181/RTSP 视频管理平台如何通过源码交付与 API 驱动节省 95% 开发成本
架构·音视频
hughnz10 小时前
油气上游IT架构的问题
架构
用户32104428194510 小时前
设计模式详解
架构
OCN_Yang11 小时前
能告诉我:你为什么用 MVI 吗?反正我不理解!
android·架构·前端框架
ai产品老杨11 小时前
深度解析:异构算力下的 AI 视频管理平台架构实现 (GB28181 / Docker / 源码交付)
人工智能·架构·音视频
我滴老baby12 小时前
工具调用全景解析从Function Calling到MCP协议的完整实践
开发语言·人工智能·python·架构·fastapi
繁星蓝雨12 小时前
Qt多界面创建的优化问题(main函数或主界面中创建?)—————附带详细方法
c++·qt·架构·多界面管理