Compose 时代的 MVI 架构:如何用单向数据流驱动复杂 UI?

前言

作为一名 Android 老兵,你一定对 MVVM 烂熟于心。但在 Compose 这种声明式 UI 环境下,随着业务逻辑的复杂度增加,MVVM 往往会暴露出一些痛点:多个 LiveData/Flow 散落在 ViewModel 中,UI 状态难以追踪,甚至出现"状态碎片化"。

为了解决这些问题,MVI (Model-View-Intent) 架构逐渐成为了 Compose 开发者的首选。它不仅是响应式编程的终极体现,更是与 Compose 的单向数据流(UDF)天生一对。

今天,我们就来实战拆解如何构建一套工业级的 MVI 架构。


一、 MVI 的三根支柱

MVI 的核心思想是将 UI 的所有交互抽象为一个闭环的单向流动:

  1. ViewState (State):UI 的唯一真实来源。它是一个包含页面所有数据的不可变对象(Data Class)。
  2. Intent (Action/Event):用户的每一个动作。比如:点击刷新、输入搜索词。
  3. Side Effect (Effect):一次性事件。比如:弹出 SnackBar、页面跳转、播放提示音。

二、 核心实现:Flow 与 ViewModel 的联姻

在 Kotlin 协程时代,我们利用 StateFlowSharedFlow 来实现 MVI 的闭环。

1. 定义状态与意图

kotlin 复制代码
// 页面状态
data class UserViewState(
    val isLoading: Boolean = false,
    val users: List<User> = emptyList(),
    val error: String? = null
)

// 用户动作
sealed class UserIntent {
    object LoadUsers : UserIntent()
    data class OnUserClick(val userId: String) : UserIntent()
}

// 一次性效果
sealed class UserEffect {
    data class ShowToast(val message: String) : UserEffect()
}

2. 构建 ViewModel 的大脑

kotlin 复制代码
class UserViewModel : ViewModel() {
    // 唯一状态暴露源
    private val _viewState = MutableStateFlow(UserViewState())
    val viewState = _viewState.asStateFlow()

    // 一次性事件通道
    private val _effect = Channel<UserEffect>()
    val effect = _effect.receiveAsFlow()

    // 处理意图
    fun onIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.LoadUsers -> loadData()
            is UserIntent.OnUserClick -> { /* 处理点击 */ }
        }
    }

    private fun loadData() {
        viewModelScope.launch {
            _viewState.update { it.copy(isLoading = true) }
            // 执行网络请求...
            _viewState.update { it.copy(isLoading = false, users = result) }
        }
    }
}

三、 UI 端的消费:状态提升 (State Hoisting) 的极致

在 Compose 页面中,消费逻辑变得极其整洁:

kotlin 复制代码
@Composable
fun UserScreen(viewModel: UserViewModel) {
    // 1. 将 Flow 转换为 Compose State
    val state by viewModel.viewState.collectAsStateWithLifecycle()

    // 2. 处理一次性 Effect
    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                is UserEffect.ShowToast -> { /* 显示弹窗 */ }
            }
        }
    }

    // 3. 渲染 UI
    UserContent(
        state = state,
        onIntent = { viewModel.onIntent(it) } // 所有的回调都聚合为一个出口
    )
}

四、 MVI 带来的架构红利

  1. 状态可回溯 : 由于整个页面的状态都缩减在一个 ViewState 对象中,你可以轻松地打印出状态变化日志,甚至实现"时光倒流(Time Travel)"式的调试。
  2. UI 纯净化 : UI 组件不再需要感知复杂的业务判断,它只需要读取 state 里的值。这极大地提升了 Compose 组件的可测试性和在 Preview 里的表现。
  3. 并发安全 : 利用协程的 update 原子操作,可以完美避开多线程修改 UI 状态导致的竞态条件(Race Condition)。

五、 给开发者的架构建议

  1. 不要过度抽象: 如果一个简单的页面只有一个开关按钮,强行上 MVI 反而会增加样板代码。MVI 适合业务逻辑复杂、状态繁多的核心页面。
  2. ViewState 的粒度控制: 如果页面巨大,建议将状态拆分为多个子状态,避免因为一个无关紧要的小状态改变(比如倒计时)导致整个页面的 Composable 函数重组。
  3. 结合 KMP 使用 : 正如我们在本专栏的项目中所做的,MVI 的 Model (State)Intent 逻辑完全可以下沉到 KMP 的 commonMain。这意味着你的 Android 和 iOS 可以共享完全相同的业务状态机。

结语

MVI 不是为了复杂而复杂,它是为了在大规模应用中保持逻辑的清晰与确定。掌握了 MVI,你就掌握了声明式 UI 架构的精髓,真正实现了"数据驱动开发"。


下一篇我们将深入探讨:Navigation Compose 深度实践:如何优雅地处理跨页跳转与传参。如果你觉得有帮助,欢迎点赞关注,我们在代码上演进,在原理上深耕。

相关推荐
望易4 小时前
刚设计的大模型架构-双域耦合认知框架
算法·架构
雨白5 小时前
指针与数组的核心机制
android
狂炫冰美式5 小时前
人均配了AI, 为什么公司还是没变快? 🤔 本质还是分布式系统问题
前端·后端·架构
她的男孩7 小时前
Spring Boot 接 Flowable 工作流:用 3 个注解搭一个请假审批流程
java·后端·架构
狗哥哥8 小时前
地图渲染模块架构设计文档
架构
黄林晴9 小时前
Room 3.0 正式发布!包名彻底重构,KMP 成为核心主线
android·android jetpack
三少爷的鞋10 小时前
Kotlin 协程环境下的 DCL 懒加载:别把线程时代的经验直接搬过来
android
plainGeekDev10 小时前
Gson → kotlinx.serialization
android·java·kotlin