一、Compose 与 MVVM:为什么说这是天作之合?

Jetpack Compose 出来之后,我身边不少 Android 开发者都在问同一个问题:MVVM 在 Compose 里还适用吗?

我的答案是:不仅适用,而且更香了。

你想想看,传统 View 体系里,我们写 Activity/Fragment,然后通过 LiveData 或 DataBinding 去驱动 UI。这套模式虽然能用,但总有些别扭——比如生命周期管理、数据绑定时的模板代码、还有那些烦人的 findViewById。

Compose 的出现,说白了就是把「数据驱动 UI」这件事做到了极致。它本身就是声明式的,天然适合 MVVM 的架构思想。我在项目中用 Compose 重构过一个老模块,代码量直接砍了一半,而且可读性提升了不少。

核心结论:Compose + MVVM = 声明式 UI 与 响应式数据的完美结合。ViewModel 负责状态管理,Compose 负责渲染,各司其职。

二、架构分层:我习惯怎么拆?

先看一张图,这是我个人在项目中沉淀下来的分层方式:

UI Layer(Compose 层) @Composable 函数 · 状态收集 · 事件回调 职责:只负责渲染 UI,不处理业务逻辑 ViewModel Layer(状态管理层) StateFlow · UiState · 业务编排 职责:管理 UI 状态,协调 UseCase Domain Layer(业务逻辑层) UseCase · Repository 接口 · 数据转换 职责:纯业务逻辑,不依赖 Android 框架 Data Layer(数据层)

嗯,这里要注意:数据流是单向的。UI 只消费状态,不直接修改数据。用户操作通过事件回调给 ViewModel,ViewModel 再更新状态,Compose 自动重组。

三、实战代码:一个完整的例子

光说不练假把式。我们写一个用户列表页面,看看 MVVM 在 Compose 里怎么落地。

3.1 定义 UI 状态

我个人习惯用 sealed class 来表示 UI 状态,这样每个状态都清晰明了:

// UiState.kt
sealed class UserListUiState {
    object Loading : UserListUiState()
    data class Success(
        val users: List<User>,
        val isRefreshing: Boolean = false
    ) : UserListUiState()
    data class Error(val message: String) : UserListUiState()
}

data class User(
    val id: Long,
    val name: String,
    val avatarUrl: String
)

我的经验:不要把 isLoading 和 data 混在一个 data class 里。用 sealed class 区分不同状态,Compose 里用 when 分支处理,代码更干净。

3.2 ViewModel 层

ViewModel 里用 StateFlow 暴露状态,用 ViewModelScope 管理协程:

// UserListViewModel.kt
class UserListViewModel(
    private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {

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

    init {
        loadUsers()
    }

    fun loadUsers() {
        viewModelScope.launch {
            _uiState.value = UserListUiState.Loading
            try {
                val users = getUsersUseCase()
                _uiState.value = UserListUiState.Success(users = users)
            } catch (e: Exception) {
                _uiState.value = UserListUiState.Error(
                    message = e.message ?: "未知错误"
                )
            }
        }
    }

    fun refresh() {
        val currentState = _uiState.value
        if (currentState is UserListUiState.Success) {
            _uiState.value = currentState.copy(isRefreshing = true)
        }
        // 刷新逻辑...
    }
}

避坑指南:我曾经在项目里直接用 MutableLiveData 暴露状态给 Compose,结果发现 Compose 重组时 LiveData 的 observe 会有延迟。后来全部换成 StateFlow,问题就解决了。StateFlow 是冷流,配合 Compose 的 collectAsState() 更丝滑。

3.3 Compose UI 层

Compose 里用 collectAsStateWithLifecycle 收集状态,生命周期安全:

// UserListScreen.kt
@Composable
fun UserListScreen(
    viewModel: UserListViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState
        .collectAsStateWithLifecycle()

    when (val state = uiState) {
        is UserListUiState.Loading -> {
            LoadingView()
        }
        is UserListUiState.Success -> {
            UserListContent(
                users = state.users,
                isRefreshing = state.isRefreshing,
                onRefresh = { viewModel.refresh() }
            )
        }
        is UserListUiState.Error -> {
            ErrorView(
                message = state.message,
                onRetry = { viewModel.loadUsers() }
            )
        }
    }
}

@Composable
fun UserListContent(
    users: List<User>,
    isRefreshing: Boolean,
    onRefresh: () -> Unit
) {
    // 这里用 LazyColumn 渲染列表
    // 下拉刷新用 PullRefresh
}

你发现没有?Compose 的 when 分支天然就是状态机。每个状态对应一个 Composable,逻辑清晰,测试也方便。

四、最佳实践清单

我在多个项目里踩过坑,总结出下面几条原则:

原则 说明 为什么重要
单向数据流 UI 只读状态,事件回调给 ViewModel 避免状态混乱,方便调试
StateFlow 替代 LiveData Compose 原生支持 Flow,生命周期安全 减少模板代码,性能更好
sealed class 定义状态 Loading/Success/Error 分开 Compose 里 when 分支更清晰
ViewModel 不持有 Composable 引用 ViewModel 只暴露 StateFlow 避免内存泄漏,可测试性高
UseCase 保持纯业务 不依赖 Android 框架 方便单元测试,架构清晰

五、一些你可能遇到的坑

说实话,Compose + MVVM 虽然好,但也不是没有坑。我挑几个常见的说说:

  • 重组过于频繁:StateFlow 的值变化太快,Compose 会频繁重组。解决方案是用 distinctUntilChanged() 过滤重复值。
  • ViewModel 的 scope 问题:Compose 里用 hiltViewModel() 获取 ViewModel,记得确保 ViewModel 的作用域是 Activity 或 NavBackStackEntry,别在 Composable 里乱创建。
  • 状态丢失:如果 ViewModel 被销毁(比如进程被杀),状态就丢了。可以用 SavedStateHandle 保存关键数据,或者用 DataStore 持久化。

一个小技巧:在 ViewModel 里用 combine 合并多个 StateFlow,可以避免多个状态源导致的重组问题。比如同时监听用户信息和网络状态,合并成一个 UiState 再暴露出去。

六、总结

Compose 和 MVVM 的结合,说白了就是「各司其职」。ViewModel 管数据,Compose 管 UI,中间用 StateFlow 搭桥。你不需要再写 DataBinding 的表达式,也不用担心 findViewById 的问题。

我在团队里推行这套架构后,新功能的开发效率明显提升了。尤其是写 UI 测试的时候,因为 ViewModel 不依赖 Compose,直接 mock UseCase 就能测业务逻辑,爽得很。

最后说一句:架构没有银弹。MVVM + Compose 适合大多数场景,但如果你遇到特别复杂的 UI 交互,也可以考虑 MVI 或者 Redux 模式。关键是找到适合你项目的方案。


公众号:蓝海资料掘金营,微信deep3321