mvi的理解

一、MVI 是什么

MVI = Model-View-Intent ,是一种单向数据流的架构模式。

它的核心思想可以用一句话概括:

View 只能"发 Intent",Model 只能"发 State",UI 只根据 State 渲染。

数据永远单向流动:

复制代码
View → Intent → ViewModel → State → View
         ↑                              ↓
         └────────── 用户操作 ──────────┘

二、为什么从 MVVM 转向 MVI

Google 官方在 Compose 时代力推 MVI,原因是 MVVM 在 Compose 下暴露了三个问题:

MVVM 的问题 MVI 的解法
多个 LiveData / StateFlow 各自更新,UI 状态不一致 只用一个 UiState 快照,原子更新
View 可以直接改 ViewModel 的属性,双向绑定混乱 View 只能 sendIntent(),不能改状态
状态分散在多个可变量里,难以调试 状态是单一不可变数据类,可完整快照

Compose 的核心是"状态驱动 UI",MVI 恰好完美契合这个模型。


三、MVI 的三大组件(Compose 视角)

1. Model(State)------ 唯一状态源

用一个不可变数据类描述页面完整状态:

kotlin 复制代码
data class RecommendUiState(
    val isLoading: Boolean = false,
    val data: List<RecommendItem> = emptyList(),
    val error: String? = null,
    val selectedIndex: Int = 0
)

关键点:

  • 用 data class 保证不可变
  • 所有字段都有默认值,方便初始状态
  • 一个页面只对应一个 UiState

2. View(Composable)------ 只读 + 只发 Intent

kotlin 复制代码
@Composable
fun RecommendScreen(
    viewModel: RecommendViewModel = koinViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // 只读 State,根据 State 渲染
    when {
        uiState.isLoading -> Loading()
        uiState.error != null -> ErrorView(uiState.error)
        else -> ContentList(uiState.data)
    }

    // 用户操作 → 发 Intent
    Button(onClick = { viewModel.sendIntent(RecommendIntent.Refresh) })
}

关键点:

  • View 不能直接改 uiState
  • View 只能通过 sendIntent() 告诉 ViewModel "我想干什么"
  • View 是 State 的纯函数:UI = f(State)

3. Intent(用户意图)------ 所有操作的统一入口

kotlin 复制代码
sealed class RecommendIntent {
    object Refresh : RecommendIntent()
    data class SelectItem(val index: Int) : RecommendIntent()
    data class PlayMusic(val position: Int, val item: RecommendData) : RecommendIntent()
}

关键点:

  • 用 sealed class 穷举所有可能的操作
  • 命名用动词,描述"用户想做什么"
  • 与 UiEffect 区分:Intent 是输入 ,Effect 是输出

四、ViewModel 的职责

ViewModel 是 MVI 的中枢,做三件事:

1. 持有唯一状态

kotlin 复制代码
private val _uiState = MutableStateFlow(RecommendUiState())
val uiState: StateFlow<RecommendUiState> = _uiState.asStateFlow()

注意 :对外只暴露 StateFlow(只读),内部用 MutableStateFlow。

2. 接收 Intent,更新 State

kotlin 复制代码
fun sendIntent(intent: RecommendIntent) {
    viewModelScope.launch {
        intentFlow.emit(intent)
    }
}

private fun handleIntent(intent: RecommendIntent) {
    when (intent) {
        is RecommendIntent.Refresh -> refresh()
        is RecommendIntent.SelectItem -> selectItem(intent.index)
        is RecommendIntent.PlayMusic -> playMusic(intent.position, intent.item)
    }
}

3. 通过 Effect 通知一次性事件

kotlin 复制代码
private val _effect = Channel<RecommendUiEffect>()
val effect = _effect.receiveAsFlow()

// 弹 Toast、导航、播放音乐等一次性事件
_effect.send(RecommendUiEffect.PlayList(list, position, tag))

State 和 Effect 的区别:

维度 UiState UiEffect
性质 状态(持久) 事件(瞬时)
示例 列表数据、加载中 弹 Toast、导航、播放
旋转屏幕 自动恢复 不会重放
消费次数 可多次读取 消费一次即消失

五、完整示例(Compose + Kotlin)

UiState

kotlin 复制代码
data class RecommendUiState(
    val isLoading: Boolean = false,
    val data: List<RecommendItem> = emptyList(),
    val error: String? = null
)

Intent

kotlin 复制代码
sealed class RecommendIntent {
    object Load : RecommendIntent()
    data class Click(val id: String) : RecommendIntent()
}

Effect

kotlin 复制代码
sealed class RecommendUiEffect {
    data class ShowToast(val msg: String) : RecommendUiEffect()
    data class Navigate(val route: String) : RecommendUiEffect()
}

ViewModel

kotlin 复制代码
class RecommendViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(RecommendUiState())
    val uiState: StateFlow<RecommendUiState> = _uiState.asStateFlow()

    private val _effect = Channel<RecommendUiEffect>(Channel.BUFFERED)
    val effect = _effect.receiveAsFlow()

    private val intentChannel = Channel<RecommendIntent>(Channel.UNLIMITED)

    init {
        viewModelScope.launch {
            intentChannel.consumeAsFlow().collect { handleIntent(it) }
        }
    }

    fun sendIntent(intent: RecommendIntent) {
        viewModelScope.launch { intentChannel.send(intent) }
    }

    private suspend fun handleIntent(intent: RecommendIntent) {
        when (intent) {
            is RecommendIntent.Load -> load()
            is RecommendIntent.Click -> click(intent.id)
        }
    }

    private suspend fun load() {
        _uiState.update { it.copy(isLoading = true) }
        try {
            val data = repository.fetch()
            _uiState.update { it.copy(isLoading = false, data = data) }
        } catch (e: Exception) {
            _uiState.update { it.copy(isLoading = false, error = e.message) }
        }
    }

    private suspend fun click(id: String) {
        _effect.send(RecommendUiEffect.ShowToast("点击了 $id"))
    }
}

Compose View

kotlin 复制代码
@Composable
fun RecommendScreen(viewModel: RecommendViewModel = koinViewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    // 消费 Effect
    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                is RecommendUiEffect.ShowToast -> toast(effect.msg)
                is RecommendUiEffect.Navigate -> navigate(effect.route)
            }
        }
    }

    // 根据 State 渲染
    when {
        uiState.isLoading -> Loading()
        uiState.error != null -> ErrorView(uiState.error)
        else -> LazyColumn {
            items(uiState.data) { item ->
                ItemView(item, onClick = {
                    viewModel.sendIntent(RecommendIntent.Click(item.id))
                })
            }
        }
    }
}

六、MVI 的核心原则(背下来)

原则 说明
单一状态源 一个页面只有一个 UiState
状态不可变 UiState 用 data class,只能通过 copy() 更新
单向数据流 View → Intent → ViewModel → State → View
View 无状态 View 不持有业务状态,只渲染 State
State 与 Effect 分离 持久状态用 State,一次性事件用 Effect
Intent 穷举 用 sealed class 穷举所有用户操作

七、State vs Effect:最容易搞混的点

这是 MVI 面试必问,一定要分清:

场景 放哪里 原因
列表数据 State 需要持久显示
加载中 State UI 需要持续显示
错误信息 State UI 需要持续显示
弹 Toast Effect 弹一次就消失
导航跳转 Effect 触发一次就够
播放音乐 Effect 触发一次就够
显示 Dialog Effect 触发一次就够

判断标准 :旋转屏幕后,这个东西还应该存在吗?

  • 应该存在 → State
  • 不应该存在 → Effect

八、MVI 在 Compose 下的完整数据流

复制代码
┌─────────────────────────────────────────────┐
│                   Composable                 │
│  ┌──────────────┐      ┌─────────────────┐  │
│  │ collectAsState│      │ sendIntent()    │  │
│  └──────┬───────┘      └────────┬────────┘  │
└─────────┼──────────────────────┼────────────┘
          │ State                │ Intent
          ▼                      ▼
┌─────────────────────────────────────────────┐
│                ViewModel                     │
│  ┌──────────────┐      ┌─────────────────┐  │
│  │ _uiState     │      │ intentChannel   │  │
│  │ (StateFlow)  │      │ (Channel)       │  │
│  └──────┬───────┘      └────────┬────────┘  │
│         │                       │            │
│         │      handleIntent()   │            │
│         └───────────┬───────────┘            │
│                     ▼                        │
│              ┌─────────────┐                 │
│              │ Repository  │                 │
│              └─────────────┘                 │
└─────────────────────────────────────────────┘

九、总结

维度 说明
核心思想 单向数据流,State 驱动 UI
三大件 UiState + Intent + Effect
ViewModel 职责 持有 State、处理 Intent、发送 Effect
View 职责 读 State 渲染、发 Intent 通知
与 Compose 的关系 Compose 的"状态驱动 UI"天然契合 MVI
与 MVVM 的区别 MVI 用单一 State 替代多 LiveData,单向数据流替代双向绑定
最易混淆点 State(持久)vs Effect(瞬时)

一句话记住 MVI:

UI = f(State),用户操作 = Intent,一次性事件 = Effect。

相关推荐
脚踏实地,坚持不懈!1 小时前
Android 电池健康度检测全流程(驱动 → 内核 → HAL → Framework)
android·linux
恋猫de小郭1 小时前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
迪康妍妍2 小时前
终端安全实战:用迪康终端安全管理系统实现U盘四分档管控与全量审计
android·运维·网络·安全·电脑
JackLam2 小时前
mobile-use 使用与接入第三方 LLM 网关完整指南OC
android·ai·mobile use
snotJam2 小时前
关于Uniapp的Android自定义基座使用
android·uni-app
TST-魚爷2 小时前
易视讯会议摄像头电视安装教程
android·电脑·钉钉·腾讯会议·zoom会议
墨天梦12 小时前
B17_LifecycleLiveData与Flow
android·kotlin
_祝你今天愉快13 小时前
UI核心 - 绘制流程(测量、布局、绘制)
android
冬奇Lab14 小时前
LLM 驱动的自动化测试系列(07):移动端自动化(三)——Mobile-Agent-v3 与自研 GUI-Owl 模型路线
android·人工智能·测试