MVI架构

MVI (Model-View-Intent) 是一种新兴的架构模式,它是 MVVM 的进化版,核心思想源自 单向数据流 (Unidirectional Data Flow) 和 函数式编程。

在 2026 年的 Android 开发生态中,随着 Jetpack Compose 成为主流,MVI 已经成为构建复杂、高可靠性应用的首选架构模式。

核心理念:单向数据流

MVI 的核心在于状态(State)是唯一的真理来源,且数据流向永远是单向的闭环:

  • View --> Intent

  • Intent --> ViewModel

  • ViewModel --> State

  • State --> View

    • end
  • 1View (视图): 只负责渲染 State 和接收用户输入。用户输入不直接触发业务逻辑,而是转化为 Intent。

  • 2Intent (意图): 描述用户想要做什么(例如:LoadUserIntent, ClickButtonIntent, SearchTextIntent)。它是 ViewModel 的唯一输入。

  • 3ViewModel (模型/处理器):

    • 接收 Intent。
    • 处理业务逻辑(调用 Model/Repository)。
    • 生成全新的、不可变的 (Immutable) State 对象。
    • 发射 State 给 View。
  • 4State (状态): 是一个不可变的数据类,包含界面渲染所需的所有数据(如:isLoading, data, errorMessage)。View 根据 State 重绘。

MVI 与 MVVM 的关键区别

虽然 MVI 和 MVVM 都有 ViewModel 和 Observer,但它们的数据处理方式截然不同:

特性 MVVM (传统) MVI (现代)
状态管理 分散式。多个 LiveData/StateFlow 分别管理不同字段 (如 userName, isLoading, error)。 集中式。只有一个单一的 State 对象,包含所有 UI 状态。
数据可变性 通常是可变的 (Mutable)。直接修改某个字段的值 (state.userName = "New")。 不可变 (Immutable)。每次更新都创建一个全新的 State 对象 (state.copy(userName = "New"))。
输入方式 调用 ViewModel 的具体方法 (viewModel.loadUser(), viewModel.onButtonClick())。 发送 Intent 事件 (viewModel.acceptIntent(LoadUserIntent))。
状态一致性 弱。由于多个独立字段,可能出现"加载中但数据已显示"的中间不一致状态。 强。State 是原子性的,同一时刻界面只能反映一种确定的状态。
调试/回放 较难追踪状态变化历史。 极易。因为所有状态变更都是通过 Intent 触发的,可以轻松记录日志、回放甚至实现"时间旅行调试"。
适用场景 简单页面,逻辑不复杂。 复杂交互、多状态并发、需要严格状态控制的页面(尤其是 Compose)。

代码实战 (Kotlin + Jetpack Compose + StateFlow)

这是 2026 年标准的 MVI 实现方式。

第一步:定义状态 (State) - 不可变

使用 data class 和 sealed class 来确保不可变性。

复制代码
// 定义所有可能的 UI 状态
data class UserUiState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val errorMessage: String? = null,
    val searchText: String = "" // 所有界面需要的数据都在这里
)

// 定义用户意图 (Intent) - 也是不可变的
sealed class UserIntent {
    object LoadUser : UserIntent()
    data class UpdateSearch(val text: String) : UserIntent()
    object Refresh : UserIntent()
}
第二步:定义 ViewModel - 状态机

使用 StateFlow 持有唯一状态,通过 reduce 模式更新状态。

复制代码
class UserViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    // 唯一的真实状态源
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow = _uiState.asStateFlow()

    // 统一入口:处理所有 Intent
    fun processIntent(intent: UserIntent) {
        viewModelScope.launch {
            when (intent) {
                is UserIntent.LoadUser -> loadUser()
                is UserIntent.UpdateSearch -> {
                    // 立即更新状态 (同步)
                    _uiState.update { it.copy(searchText = intent.text) }
                    // 可以触发搜索逻辑...
                }
                is UserIntent.Refresh -> loadUser()
            }
        }
    }

    private suspend fun loadUser() {
        // 1. 设置加载状态 (生成新 State)
        _uiState.update { it.copy(isLoading = true, errorMessage = null) }

        try {
            // 2. 请求数据
            val user = userRepository.getUser()
            
            // 3. 设置成功状态 (生成新 State)
            _uiState.update { it.copy(isLoading = false, user = user) }
        } catch (e: Exception) {
            // 4. 设置错误状态 (生成新 State)
            _uiState.update { it.copy(isLoading = false, errorMessage = e.message) }
        }
    }
}
第三步:定义 View (Compose) - 渲染与反馈

View 只是状态的函数:UI = f(State)。

复制代码
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    // 收集状态流
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    // 渲染 UI (完全由 state 决定)
    Box {
        if (state.isLoading) {
            CircularProgressIndicator()
        } else if (state.errorMessage != null) {
            Text(text = "错误: {state.errorMessage}")
            Button(onClick = { viewModel.processIntent(UserIntent.Refresh) }) {
                Text("重试")
            }
        } else {
            Column {
                Text(text = "用户名: {state.user?.name}")
                TextField(
                    value = state.searchText,
                    onValueChange = { 
                        // 用户输入转化为 Intent
                        viewModel.processIntent(UserIntent.UpdateSearch(it)) 
                    }
                )
            }
        }
    }
}

MVI 的优缺点分析

✅ 优点

  • 1可预测性 (Predictability):由于状态是不可变的且集中管理,给定一个初始状态和一个 Intent,结果状态是确定的。这极大地减少了"由于时序问题导致的 Bug"。
  • 2调试友好:你可以轻松打印每一个 Intent 和对应的 State 变化,甚至可以录制这些事件并在测试中回放。
  • 3配置变更安全:屏幕旋转时,ViewModel 保留,State 保留,UI 自动恢复,无需手动保存 Bundle。
  • 4完美契合 Compose:Jetpack Compose 的声明式 UI 本质就是 State -> UI,MVI 的单一状态流与 Compose 是天作之合。
  • 5解耦:View 不知道业务逻辑,只负责发 Intent 和画 State;ViewModel 不知道 View 的具体实现。

❌ 缺点

  • 1样板代码 (Boilerplate):需要定义大量的 State 类和 Intent 类(Sealed class),对于简单页面显得繁琐。
  • 2内存开销:每次状态更新都创建新对象。虽然在现代 Kotlin/JVM 优化下影响很小,但在极高频率更新(如每秒 60 帧的动画坐标)场景下需注意。
  • 3学习曲线:开发者需要理解"不可变性"、"归约 (Reduce)"、"单向数据流"等函数式编程概念。
  • 4过度设计风险:对于极其简单的静态页面,强行上 MVI 可能杀鸡用牛刀。

常见实现库与模式变体

在实际开发中,我们通常不会手写所有的 update/copy 逻辑,而是借助库:

  • 官方推荐: StateFlow + ViewModel (如上例)。
  • Orbit-MVI: 一个流行的库,专门简化 MVI 的实现,提供了 container 概念来处理副作用和状态归约。
  • Redux/Kotlin Redux: 直接移植前端 Redux 理念到 Android。
  • Hilt/Dagger: 用于依赖注入,配合 MVI 使用。

总结:什么时候用 MVI?

  • 强烈推荐:

    • 使用 Jetpack Compose 的新项目。
    • 页面逻辑复杂,状态多变(如:加载、成功、失败、空数据、分页、多条件筛选同时存在)。
    • 对稳定性要求极高,需要严格测试状态流转的金融/交易类应用。
    • 团队希望统一架构规范,减少"神奇 Bug"。
  • 可以考虑不用:

    • 极其简单的静态展示页(如"关于我们")。
    • 纯动画或高性能图形渲染场景(此时状态更新频率过高,可能需要更底层的控制)。

一句话总结:

MVI 是通过不可变状态和单向数据流,将 UI 变成确定性函数的架构模式。它是 Jetpack Compose 时代的最佳拍档,解决了 MVVM 中状态分散和一致性问题,是现代 Android 架构的终极形态之一。

相关推荐
森叶2 小时前
深入理解 Hash:它不是一个函数,而是一种思想
人工智能·http·架构
呆子也有梦2 小时前
思考篇:积分是存成道具还是直接存数值?——ET/Skynet 框架下,从架构权衡到代码实现全解析
游戏·架构·c#·lua
学嵌入式的小杨同学2 小时前
STM32 进阶封神之路(十八):RTC 实战全攻略 —— 时间设置 + 秒中断 + 串口更新 + 闹钟功能(库函数 + 代码落地)
c++·stm32·单片机·嵌入式硬件·mcu·架构·硬件架构
fajianchen2 小时前
如何设计微服务统一认证中心
微服务·云原生·架构·iam
于先生吖2 小时前
基于 Java 开发短剧系统:完整架构与核心功能实现
java·开发语言·架构
jaysee-sjc3 小时前
十六、Java 网络编程全解析:UDP/TCP 通信 + BS/CS 架构
java·开发语言·网络·tcp/ip·算法·架构·udp
猹叉叉(学习版)3 小时前
【ASP.NET CORE】 14. RabbitMQ、洋葱架构
笔记·后端·架构·c#·rabbitmq·asp.net·.netcore
独断万古他化3 小时前
【抽奖系统开发实战】Spring Boot 抽奖系统全链路总结:从架构到落地的实践复盘
java·spring boot·后端·架构·系列总结