一、Compose 与 MVVM:为什么说这是天作之合?
Jetpack Compose 出来之后,我身边不少 Android 开发者都在问同一个问题:MVVM 在 Compose 里还适用吗?
我的答案是:不仅适用,而且更香了。
你想想看,传统 View 体系里,我们写 Activity/Fragment,然后通过 LiveData 或 DataBinding 去驱动 UI。这套模式虽然能用,但总有些别扭——比如生命周期管理、数据绑定时的模板代码、还有那些烦人的 findViewById。
Compose 的出现,说白了就是把「数据驱动 UI」这件事做到了极致。它本身就是声明式的,天然适合 MVVM 的架构思想。我在项目中用 Compose 重构过一个老模块,代码量直接砍了一半,而且可读性提升了不少。
核心结论:Compose + MVVM = 声明式 UI 与 响应式数据的完美结合。ViewModel 负责状态管理,Compose 负责渲染,各司其职。
二、架构分层:我习惯怎么拆?
先看一张图,这是我个人在项目中沉淀下来的分层方式:
嗯,这里要注意:数据流是单向的。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