搜狐技术产品

聊聊 MVI 的发展史和现代 Android 实践

1. 引言:抛弃刻板印象

MVI 是 Android 开发中绕不开的架构模式。它提供清晰、可预测的数据流,是构建稳定 UI 的得力工具。 但现实中,“MVI” 常被误解为单一、僵化的模式。有人将其与 “Redux 风格实现” 划等号,导致很多团队因对 Redux 的刻板印象而回避 MVI。其实 MVI 针对 Android 场景进行了灵活适配,能够在更加易用的同时保留其核心思想。

本文将探索 MVI 的根源、它要解决的问题,以及拥抱其 “适应性” 而非 “僵化规则” 的重要性。架构的意义在于解决问题,而非盲目遵循教条。

2. 架构先驱:孕育 MVI 的模式

MVI 不是凭空出现的,它是架构演进的产物。在它之前,已有诸多模式为其铺垫,开发者们不断反思现有方案,推动着架构的迭代(参考 [1][2][3])。

很多人认为 MVI 源自 Redux(2015),但追根溯源,它的真正先驱是:Model-View-Controller(MVC)和后来的响应式编程模式。

1979年: Model-View-Controller(MVC)

MVC 是最古老、最具影响力的架构模式之一,诞生于 20 世纪 70 年代。它将应用拆分为三大组件:

  • Model:管理数据和业务逻辑
  • View:处理用户输入(如点击)
  • Controller:处理用户输入,驱动 Model 更新 
Image

2014年5月:Flux

2014 年,Facebook 推出了 Flux,聚焦数据流向管理。它的核心是:

  • Actions:描述发生的事件
  • Dispatcher:分发 Actions 到 Store
  • Store:持有应用状态,响应 Actions 更新状态
  • Views:监听 Store 变化,触发重新渲染

Flux 最关键的创新是 “单向、循环数据流”,解决了共享可变状态的问题。这种 “可预测、线性” 的数据流,成为 MVI 的核心思想。

Image

2014年12月:MVI(响应式 MVC)

André Staltz 从响应式编程视角重新审视 MVC。在其博客 《Reactive MVC and the Virtual DOM》[4] 中,提出 “响应式 MVC” 概念。

MVI 借鉴了 Flux 的单向数据流,简化了实现:去掉 Dispatcher,让 Intent 直接驱动 Model,用可观测流连接 Model、用户意图、状态和 UI 渲染,形成 “纯响应式转换”:

  • Intent 替代 Controller:将用户交互转化为事件流
  • Model 响应事件流,更新应用状态
  • View 监听状态流,触发渲染

这种重构后的架构被称为 Model-View-Intent(MVI),这是 MVI 模式的首次正式提出。它继承了 MVC 的 “单向数据流”,并天然适配响应式编程的 “可观测流”。

Image

3. MVI 的起源:模式如何成型

MVI 模式由 André Staltz 在 2014 年(Redux 诞生前)首次提出,最初用于 Cycle.js 框架,而非 Android。直到 2015 年 Redux 流行后,MVI 才在 Android 生态中被广泛讨论。

MVI 模式的实践定义

Model-View-Intent(MVI)是一种响应式架构模式,强制实现 “单一、不可变的状态” 和 “单向事件流”,核心组件:

  • Model:持有应用状态
  • View:渲染状态
  • Intent:替代 Controller,作为用户交互的事件流 (此处省略 MVI 流程图描述,核心逻辑:用户交互生成 Intent,Intent 驱动 Model 生成新状态,View 监听状态更新并渲染。)

我们可以从 André Staltz 2015 年的博客《Unidirectional User Interface Architectures 》[5]中,阅读 MVI 的定义和组件描述。

他的设计聚焦 “最小化用户意图 → 状态变化 → UI 渲染” 的循环,形式化了数据流,让状态变化可预测。这种思想借鉴了 MVC、Flux、Redux(后出现),但 MVI 是为响应式系统量身定制的独特架构。

代码示例

2015 年,Jannis Dornemann 在博客中,用数学表达式诠释了 MVI 的核心思想:

Image

对应的代码实现逻辑大致如下(伪代码):

// Intent 是用户交互事件流  
val intents: Flow<Intent> = view.userInteractions()  

// Model 处理 Intent,生成状态流  
val states: Flow<State> = intents.map { intent ->  
    model.reduce(intent)  
}  

// View 监听状态流,渲染 UI  
states.collect { state ->  
    view.render(state)  
}  

你可以在代码中看到这个数学表达式的直接体现。

不过,这个 “理想模型” 如何真正落地到 Android 开发中?别着急,我们继续拆解。

4. 误区:MVI 不是什么

随着 MVI 在 Android 生态流行,一个常见误区出现了:“MVI 必须是 Redux 风格的单向状态管理,必须有全局 Store、Reducer”。甚至有人质疑:“如果 Android 实现没有这些,能叫 MVI 吗?”

误区 1:MVI = Redux

最常见的误解是 “MVI 就是 Redux”。但事实是:

  • MVI 由 André Staltz 在 2014 年在博客文章 《Reactive  MVC》[6] 提出,远早于 2015 年 Dan Abramov 发布的 Redux[7]。
  • MVI 的核心思想(单向数据流、不可变状态)确实启发了 Redux,但两者有本质区别。

Redux 是 “集中式全局状态管理”,而 MVI 是 “分布式状态管理(每个模块 / 页面独立)”。两者的相似点(如状态不可变、基于事件流)下,隐藏着架构差异:

概念
Redux
MVI
状态管理
集中式全局状态
分布式(每个模板/页面独立)
Reducer
单一全局 Reducer 处理所有 Action
每个模块 / 页面有独立 Reducer
数据流
dispatch(Action) → Store → Reducer → State
Intent → Model → New State → View
控制逻辑
外部 Dispatcher 分发 Action
模块内部响应式处理 Intent

简言之:Android 中的 MVI 实现,不一定要严格遵循 Redux 模式。

误区 2:MVI = MVVM(或其他模式)

另一个误区是 “MVI 只是换皮的 MVVM”。 MVVM 架构曾经一度被 Android 官方强推,下面是官方经典 MVVM 架构图:

Image

很多开发者在实践中,不自觉地用 MVI 的方式写 MVVM(或反之),因为两者确实有相似基础:

  • 都用可观测流传递状态
  • 都用不可变状态更新 UI
  • 都强调 “响应式编程”(尤其是 Android 中的 Jetpack 组件)

相似性背后,核心差异在于数据流和架构约束:

概念
MVVM
MVI
状态
分散(多个 LiveData/StateFlow)
单一(每个模块一个不可变状态)
数据流
双向(View → ViewModel → Model)
单向(Intent → Model → State → View)
事件
零散(ViewModel 直接处理事件)
集中(Intent 作为事件流统一处理)
Reducer
无强制要求
强制(状态通过 Reducer 生成)

这些差异的存在源于 MVI 和 MVVM 的本质的区别:View 是否持有状态。MVVM 的 View 持有状态,所以需要与 ViewMoel 双向通信,而 MVI 追求所有状态管理发生在 ViewModel。

当你 MVVM 代码中已经没有 DataBinding、没有需要向 ViewModel 传递参数(状态)的代码时,实际上就是一段准 MVI 代码了。

下面是一段用 MVVM 实现的 MVI 代码示例,也是当今项目中非常流行的一种写法:

// ViewModel exposing multiple state properties
classUserViewModel : ViewModel() {
// Backing properties
privateval _username = MutableStateFlow("")
val username: StateFlow<String> = _username.asStateFlow()

privateval _age = MutableStateFlow(0)
val age: StateFlow<Int> = _age.asStateFlow()

// Functions to update state
funonUsernameChanged(newName: String) {
        _username.value = newName
    }

funonAgeIncrement() {
        _age.value += 1
    }

funonAgeDecrement() {
if (_age.value > 0) _age.value -= 1
    }
}

@Composable
funUserScreen(viewModel: UserViewModel = viewModel()) {
// Collect state from the ViewModel
val username by viewModel.username.collectAsStateWithLifecycle()
val age by viewModel.age.collectAsStateWithLifecycle()

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = username,
            onValueChange = { viewModel.onUsernameChanged(it) },
            label = { Text("Username") }
        )

        Text(text = "Age: $age")

        Button(onClick = { viewModel.onAgeIncrement() }, modifier = Modifier.padding(top = 8.dp)) {
            Text("Increase Age")
        }
        Button(onClick = { viewModel.onAgeDecrement() }, modifier = Modifier.padding(top = 8.dp)) {
            Text("Decrease Age")
        }
    }
}

这样的代码很实用,没有大问题,但是在约束性上稍显不足,仍然有提升空间。真正的 MVI 应严格遵循 “单向数据流、不可变状态、Intent 驱动” 的约束。

5. 真正的 MVI 应该怎么写

通过分析一些常见误解,结合相关模式,我们可总结出 真正 MVI 的核心特征:

  1. 单一不可变状态模型:状态是集中、不可变的,通常封装在 ViewState 中。
  2. 状态托管在 Model 中:ViewState 由 Model 管理,而非分散在 View 层。
  3. Intent 作为用户交互的统一抽象:所有用户操作(点击、输入等)都封装为 Intent,作为事件流传递。
  4. Intent 触发状态转换:Intent 传入 Model 后,通过 Reducer 生成新状态。
  5. 单向数据流:严格遵循 Intent → State → View 的流向,禁止反向依赖。

MVI 所需的数据模型

建议 MVI框架中都有以下几个数据模型,加强框架的可约束性

  • UiState:定义 UI 关注的 State,注意需要是 Immutable 的。
  • Intent:定义来自 UI 的 Action
  • SideEffect:剥离那些纯业务逻辑之外的数据状态为副作用,比如日志、埋点等
// UI State data class (immutable)
dataclassUserUiState(
val username: String = "",
val age: Int = 0,
val isLoading: Boolean = false
)

// User intents: user actions
sealedinterfaceUserIntent{
dataclassChangeUsername(val newName: String) : UserIntent
object IncrementAge : UserIntent
object DecrementAge : UserIntent
}

// Side effects (one-time events)
sealedinterfaceUserSideEffect{
dataclassShowMessage(val message: String) : UserSideEffect
}

MVI 中 ViewModel 的职责

在 Android 中,ViewModel 通常承担以下职责:

  • 暴露可监听的 State(如 StateFlow<ViewState>),主要务必使 Immutable 的
  • 提供公共函数接收和处理 Intent,通过应用 reducer 逻辑更新 State。:如网络请求、数据库操作,结果转化为 Intent 或直接更新 State
  • 可以在 StateFlow 之外同一个,处理副作用的 Flow,确保 reducer 逻辑是纯函数
classUserViewModel : ViewModel() {
privateval _sideEffects = MutableSharedFlow<UserSideEffect>()
val sideEffects = _sideEffects.asSharedFlow()

privateval _uiState = MutableStateFlow(UserUiState())
val uiState = _uiState.asStateFlow()

// Process a single intent immediately
funprocessIntent(intent: UserIntent) {
when (intent) {
is UserIntent.ChangeUsername -> {
                _uiState.update { it.copy(username = intent.newName) }
            }
            UserIntent.IncrementAge -> {
                _uiState.update { it.copy(age = it.age + 1) }

                viewModelScope.launch {
                    _sideEffects.emit(UserSideEffect.ShowMessage("Age increased!"))
                }
            }
            UserIntent.DecrementAge -> {
if (current.age > 0) {
                    _uiState.update { current ->
                        current.copy(age = current.age - 1)
                    }
                }
            }
        }
    }
}

为什么 MVI 难以 “标准化”?

MVI 没有官方标准实现,因为它本质是 “架构思想”,而非 “固定模板”。不同团队可根据需求调整(如是否用全局 Store、如何处理副作用),但核心原则不能丢。

这也导致一个矛盾:开发者可能因 “没有统一标准” 而困惑,但正是这种灵活性,让 MVI 适配不同场景。

6. 最终思考: 如何选型?

  1. “是否值得用” 取决于团队和场景:如果你是 solo 开发者,MVI 的约束可能显得冗余;但团队协作时,它的 “可预测性” 能大幅降低沟通成本。

  2. 不要为了 “用 MVI” 而用 MVI:先理解问题,再选工具。如果项目简单,MVVM 或 MVC 足够;复杂场景下,MVI 的优势才会凸显。

  3. MVI 考验的不只是技术,更是 “软件工程思维”:它强制你思考 “状态如何变化”“事件如何流转”,倒逼开发者写出更整洁、可维护的代码。

  4. MVI 不一定要用 Redux 式全局 Store:小模块中,MVI 可轻量化实现(如单个 ViewModel + 局部 State),关键是遵循核心原则。

  5. 别纠结 “是不是真正的 MVI”:模式的价值在于解决问题。只要你的架构符合 “单向数据流、不可变状态、Intent 驱动” 的核心思想,就是 “MVI 精神” 的实践。

参考资料
[1] 

André Staltz 博客:Architectural patterns for interactive programs: https://staltz.com/some-problems-with-react-redux

[2] 

Nothing new in React and Flux except one thing: https://staltz.com/nothing-new-in-react-and-flux-except-one-thing

[3] 

Unidirectional User Interface Architectures: https://staltz.com/unidirectional-user-interface-architectures#:~:text=Model-Vie

[4] 

Reactive MVC and the Virtual DOM: https://www.futurice.com/blog/reactive-mvc-and-the-virtual-dom

[5] 

Unidirectional User Interface Architectures: https://staltz.com/unidirectional-user-interface-architectures#:~:text=Model-View-Intent

[6] 

Reactive  MVC: https://www.futurice.com/blog/reactive-mvc-and-the-virtual-dom

[7] 

Redux: https://redux.js.org/understanding/history-and-design/history-of-redux#2015-the-birth-of-redux