前言
MVI(Model-View-Intent)最早由 André Staltz 于 2015 年在他的 Cycle.js 库中提出。相比 Vue、React、Redux 等主流框架,这是一个相对小众但极具特色的架构模式,主要针对 MVC 的痛点进行改进。
André Staltz 在 JSConf Budapest 2015 上发表了关于 MVI 的演讲:What if the user was a function?,感兴趣的朋友可以观看,深入了解 MVI 的设计理念和演进历程。值得一提的是,André Staltz 也是 RxJS 早期的核心贡献者。
在 Android 领域,最早将 MVI 引入的应该是 Hannes Dorfmann。受到 Cycle.js 的启发,Hannes 萌生了将 MVI 应用于 Android 开发的想法,并开发了 Mosby——一个基于 MVI 模式的 Android 框架。
基本概念
响应式编程(Reactive Programming)是一种面向数据流和变化传播的声明式编程范式。它允许开发者方便地表达静态或动态的数据流,计算模型会自动将变化的值通过数据流进行传播。 —— 维基百科

MVI 是一种纯响应式、函数式编程的架构:
- Model 监听 Intent
- View 监听 Model
- Intent 由 View 发出
数据单向流动,每个组件都类似函数式编程中的纯函数——接收输入,产生输出,传递给下一个环节。因此,Android 上的 MVI 实现通常需要借助 LiveData、RxJava、Kotlin Flow 等响应式框架。
下图完美诠释了 MVI 的函数式编程思想:

用代码简单描述这个循环:
fun view(model: (action: Action) -> Data) = {
UserInput(model(action))
}
fun model(intent: (input: UserInput) -> Action) = {
Action(intent(input))
}
fun intent(view: (data: Data) -> UserInput) = {
UserInput(view(data))
}
fun main() {
view(model(intent()))
}
MVI 三要素
Intent(意图)
- 输入:用户在屏幕上的交互行为
- 输出:描述用户行为的数据模型,例如用户触发刷新时,输出
RefreshAction(params)
Model(数据层)
- 输入:描述用户行为的数据(来自 Intent)
- 输出:供 View 层渲染的数据
View(视图层)
- 输入:来自 Model 的数据
- 输出:用户的点击、滑动等交互事件
MVI 强调数据单向闭环流动,以纯函数式的方式驱动这个循环,每个组件严格遵守输入输出规则。整个循环的起点通常是 View,因为一切都从用户交互开始。
函数式编程与副作用(Side Effect)
Cycle.js 提出的 MVI 强调函数式编程,三个组件只关心输入参数和输出结果。
函数式编程是一种编程范式,它将计算机运算视为数学函数运算,避免使用程序状态和可变对象。λ 演算是该范式的重要理论基础,λ 演算的函数可以接受函数作为参数,也可以返回函数。 —— 维基百科
常见的函数式编程语言有 Haskell、Scala、F#,各种响应式框架(RxJava、RxSwift、RxJS)也都采用函数式编程思想。
纯函数的特征
函数式编程分为纯函数和非纯函数。纯函数必须满足:
- 不访问程序外部的状态和可变数据
- 不对函数外部产生影响
- 相同的输入必然产生相同的输出(引用透明性)
- 不调用任何非纯函数
举个例子:
var b = 2
fun add(a: Int): Int {
b = a + 1 // ❌ 修改了外部变量
return b
}
这个 add 方法访问并修改了外部变量 b,因此不是纯函数。
纯函数是相对独立的,只有明确的输入和输出,不影响任何外部数据,且输入输出数据都是不可变的。
什么是副作用?

按照响应式和函数式编程的规则,MVI 闭环理论上不应对外部环境产生任何影响——因为 MVI 的三个组件是一个整体,按顺序生产和消费数据。
但实际应用中,我们需要额外的输出。例如:
- 用户下拉刷新产生 Intent
- Intent 传递给 Model
- Model 调用 API 请求数据
- 数据输出给 View
- View 渲染 UI(这是副作用)
渲染 UI 属于 View 函数产生的副作用,因为它不属于 View 函数的输出,而是一个额外产出。它只是监听数据并渲染,不产生返回值进入下一个环节。
因此,我们引入了 Side Effect 的概念来处理这类场景。熟悉 Jetpack Compose 的开发者会发现,Compose 中也有 Side Effect API,原理是相同的。可以说,所有基于函数式编程的 UI 框架都需要这个概念。
状态(State)
基于纯函数式编程的思想,我们需要遵守不可变原则。理解了 MVI 的工作流程后,让我们深入探讨状态的概念。
什么是状态?
对于前端(包括移动端)开发,状态指的是视图的状态,即对页面抽象出的数据结构。在 MVI 架构中,状态指 Model 层输出的数据,View 层通过消费状态数据来渲染页面 UI。
例如,一个列表页面的状态可以定义为:
data class MainPageState(
val list: List<Item> = emptyList(),
val page: Int = 0,
val totalSize: Int = 0,
val isLoading: Boolean = false
)
View 层通过监听状态变化来渲染 UI:
class MainView {
fun init() {
// 建立观察者模式,监听 state 的变化
viewModel.observeStateChanged(this) { state ->
renderUI(state)
}
}
fun renderUI(state: MainPageState) {
// 根据状态渲染页面
}
}
状态的不可变性
View 依赖 State 更新,因此作为 View 的唯一可信数据源,State 必须是不可变的。想要更新 View,必须生成新的 State。
不仅是 State,MVI 中所有的输入输出参数都必须不可变。如果函数式编程中的参数可以随意更改,代码的其他部分无法感知,就会产生难以追踪的 bug。可变意味着不可信。
Reducer 与状态管理
MVI 的 View 层自身不允许管理状态,只能将状态作为输入参数进行监听。状态的产生是连续的,前后状态必然相互关联,因此需要一个队列来管理状态,确保 View 层按顺序处理,不丢失任何状态。
谁来管理状态?
按照 MVI 的架构设计,显然应该由生产 State 的 Model 层来管理。
以列表页加载更多为例:加载下一页时,需要将当前页码 page + 1 作为参数请求数据,新状态的列表需要合并当前列表和新数据:
class MainPageModel {
private val stateStore = StateStore()
fun loadMore() {
val currentState = stateStore.getCurrentState()
val nextPage = currentState.page + 1
val newList = RemoteApi.getList(nextPage)
val nextState = currentState.copy(
list = currentState.list + newList,
page = nextPage
)
stateStore.add(nextState)
}
}
与 Redux 中心化的状态管理不同,MVI 中每个模块都是独立的闭环,因此每个页面的 Model 层拥有独立的状态管理。对于复杂页面,甚至可以拆分为多个 Model,分别管理不同的状态。
Reducer 的概念
从上面的示例可以看出,新状态的产生需要 Intent 数据和当前状态共同参与。这个过程被称为 Reducer(可以理解为"归约"或"合并",将 Action 数据和当前状态合并成新状态)。
完整流程如下:

Reducer 可以定义为:
fun reducer(action: Action, state: State): State {
// 根据 action 和当前 state 计算新 state
return newState
}
Reducer 的纯函数要求
许多 MVI 文章中没有明确 Reducer 和状态管理者的角色,而是直接解析 Action 后更新 State。这有两个问题:
- 不符合 MVI 原始设计:Reducer 和状态管理是 MVI 的灵魂部分
- 状态输出无序:状态的产生应该是有序的(排除异步副作用部分),因此应使用队列存储状态函数,按顺序执行
保持 Reducer 的纯净至关重要!不要在 Reducer 函数中做以下操作:
- ❌ 修改传入参数
- ❌ 执行有副作用的操作(如 API 请求、路由跳转)
- ❌ 调用非纯函数(如
Date.now()、Math.random())
作为纯函数,Reducer 只要传入相同的参数,必然产生相同的输出。
Reducer 的职责边界
以刷新列表为例:
- 用户触发刷新 Action
- Model 调用 API 请求数据
- 请求开始时,通过 Reducer 生成 loading 状态
- 请求完成后,通过 Reducer 生成包含数据的新状态
整个过程中,Reducer 只作为纯粹的状态计算工具:
- 不关心如何请求数据
- 不关心如何渲染数据
- 只负责根据输入产生新状态
角色分工非常明确。
StateStore 的实现
为了保证状态产出的顺序,StateStore 需要:
- 保存 Reducer 函数及其所需的参数
- 可选:保存产出的 State 及其 Action,便于调试和状态追溯
MVI 完整实现示例
基础版本
// 状态管理器
class StateStore<S> {
private val queue = LinkedBlockingQueue<Pair<Any, (Any, S) -> S>>()
fun add(actionData: Any, reducer: (Any, S) -> S) {
queue.offer(Pair(actionData, reducer))
}
fun poll(): Pair<Any, (Any, S) -> S>? = queue.poll()
}
// ViewModel 层
class MainViewModel : ViewModel() {
private val stateStore = StateStore<MainPageState>()
// 当前状态
private val _state = MutableStateFlow(MainPageState())
val state: StateFlow<MainPageState> = _state.asStateFlow()
init {
// 在协程中有序消费 Reducer
viewModelScope.launch(Dispatchers.IO) {
while (isActive) {
stateStore.poll()?.let { (action, reducer) ->
val newState = reducer.invoke(action, _state.value)
// 过滤无意义的重复状态
if (_state.value != newState) {
_state.value = newState
}
}
delay(10) // 避免空轮询
}
}
}
fun loadList(refresh: Boolean) {
val currentState = _state.value
if (currentState.isLoading) return
val page = if (refresh) 0 else currentState.page + 1
// 设置加载状态
stateStore.add(page) { pageNum, state ->
(state as MainPageState).copy(isLoading = true)
}
// 异步加载数据
viewModelScope.launch {
try {
val moreList = Repo.getList(page)
stateStore.add(Pair(page, moreList)) { data, state ->
val (p, list) = data as Pair<Int, List<Item>>
val currentList = (state as MainPageState).list
state.copy(
isLoading = false,
list = if (p == 0) list else currentList + list,
page = p
)
}
} catch (e: Exception) {
stateStore.add(e) { error, state ->
(state as MainPageState).copy(isLoading = false)
}
}
}
}
}
// View 层
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 监听状态变化
lifecycleScope.launch {
viewModel.state.collect { state ->
renderUI(state)
}
}
}
private fun renderUI(state: MainPageState) {
if (state.isLoading) {
showLoading(state.page == 0)
return
}
updateList(state.list)
}
fun onCloseClick() {
val state = viewModel.state.value
if (state.isEditMode) {
viewModel.cancelEdit()
return
}
finish()
}
}
简化版本(推荐)
实际开发中,为了便利性,可以弱化 Action 参数,直接在 Reducer 中使用外部的不可变值(通过闭包捕获)。这种方式虽然严格意义上不是纯函数,但实践中可以接受。
Reducer 可以简化定义为:
typealias Reducer<S> = S.() -> S
使用 Kotlin 的扩展函数语法,Reducer 内部可以直接访问当前 State:
class StateStore<S> {
private val queue = LinkedBlockingQueue<S.() -> S>()
fun add(reducer: S.() -> S) {
queue.offer(reducer)
}
fun poll(): (S.() -> S)? = queue.poll()
}
class MainViewModel : ViewModel() {
private val stateStore = StateStore<MainPageState>()
private val _state = MutableStateFlow(MainPageState())
val state: StateFlow<MainPageState> = _state.asStateFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
while (isActive) {
stateStore.poll()?.let { reducer ->
val newState = reducer.invoke(_state.value)
if (_state.value != newState) {
_state.value = newState
}
}
delay(10)
}
}
}
fun loadList(refresh: Boolean) {
if (_state.value.isLoading) return
val page = if (refresh) 0 else _state.value.page + 1
// 设置加载状态
stateStore.add {
copy(isLoading = true)
}
viewModelScope.launch {
try {
val moreList = Repo.getList(page)
// 更新数据(闭包捕获 page 和 moreList)
stateStore.add {
copy(
isLoading = false,
list = if (page == 0) moreList else list + moreList,
page = page
)
}
} catch (e: Exception) {
stateStore.add {
copy(isLoading = false)
}
}
}
}
}
至此,一个完整的 MVI 架构雏形已经形成。
总结
MVI 的核心要素
- ✅ 响应式与函数式编程
- ✅ 状态管理与 Reducer
- ✅ 单向数据流
- ✅ View 无内部状态(状态只能存在 Model 中)
注意:虽然 Flutter/Compose/Vue 的 UI 组件可以有内部状态,但在 MVI 架构中,应用级状态只能存在于 Model。
优点
- 架构清晰解耦:思想简单,能将复杂业务简化,易于维护
- 可测试性强:每个组件都是纯函数,容易编写单元测试
- 可调试性强:可以记录状态变化历史,还原任意时刻的 State,追溯用户操作路径定位问题
- 可预测性高:单向数据流保证了状态变化的可预测性
缺点
- 性能开销:不断生成不可变对象对性能有轻微影响(实际项目中可以忽略)
- 复用性有限:ViewModel 和 State 通常是页面特定的,复用性不强
- 学习曲线:需要理解函数式编程和响应式编程的概念
- 依赖 Diff 机制:需要高效的 UI Diff 组件(如 RecyclerView 的 DiffUtil、Compose 的重组机制)
适用场景
个人认为,MVI 非常适合现代声明式 UI 框架,是未来的发展趋势:
- Flutter:使用 pub.dev/packages/vi… 中的 StateViewModel 管理状态
- Jetpack Compose:推荐使用 Airbnb Mavericks
- 传统 View 体系:可以尝试 Airbnb Epoxy(声明式 RecyclerView)搭配 Mavericks
推荐学习资源
以上是我个人对 MVI 的学习总结,权当抛砖引玉。以下是精选的优质学习资源:
理论基础
-
Cycle.js - Model-View-Intent
从源头了解 MVI 的设计理念 -
What if the user was a function? - JSConf 2015
Cycle.js 作者关于 MVI 的经典演讲 -
Reactive MVC and the Virtual DOM
深入阐述 MVI 的设计思想和优势
Android 实践
-
Mosby3-MVI 系列文章
MVI 在 Android 的首次应用,7 篇文章结合实例详细讲解 -
Airbnb Mavericks
Airbnb 开源的成熟 MVI 框架,字节跳动教育团队也在使用 -
Badoo MVICore
知名社交应用 Badoo 开源的 MVI 框架