45、Compose与ViewModel:在Compose中使用ViewModel、collectAsState、LaunchedEffect
说实话,Compose 刚出来那会儿,我第一反应是:这玩意儿跟 ViewModel 怎么配合?毕竟 ViewModel 是 Android 架构组件的核心,如果 Compose 不能优雅地用它,那落地就难了。后来用了一段时间,我发现 Compose + ViewModel 的组合,其实比传统的 XML + ViewModel 要顺手得多。今天我就把这块的核心用法掰开揉碎讲清楚。
ViewModel 在 Compose 中的基本用法
在 Compose 里获取 ViewModel 非常简单,直接用 viewModel() 函数就行。这个函数是 Compose 的扩展,它会自动帮你管理 ViewModel 的生命周期。
@Composable
fun MyScreen() {
val viewModel: MyViewModel = viewModel()
// 使用 viewModel 中的数据
}
嗯,这里要注意一点:viewModel() 只能在 Composable 函数里调用。如果你在非 Composable 的地方需要 ViewModel,那就得用传统方式了。我在项目中遇到过有人试图在普通函数里调用 viewModel(),结果直接崩溃——因为 Compose 的运行时上下文不存在。
collectAsState:把 Flow 变成 Compose 的状态
ViewModel 里最常用的数据源就是 StateFlow 或 LiveData。在 Compose 中,我们需要把它们转换成 Compose 的 State 对象,这样 UI 才能自动重组。
我个人习惯用 StateFlow + collectAsState() 的组合。看个例子:
class MyViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen() {
val viewModel: MyViewModel = viewModel()
val count by viewModel.count.collectAsState()
Column {
Text("当前计数:$count")
Button(onClick = { viewModel.increment() }) {
Text("加一")
}
}
}
这里用 by 委托语法,直接把 StateFlow 变成了 Compose 的 State。说白了,collectAsState() 就是一个桥梁,把 Flow 的发射值映射成 Compose 可观察的状态。每当 _count.value 变化,UI 就会自动更新。
observeAsState() 替代 collectAsState()。但我建议尽量迁移到 StateFlow,因为它在协程世界里更自然。
LaunchedEffect:处理副作用的好帮手
Compose 里有个概念叫「副作用」——说白了就是那些不直接参与 UI 渲染,但又必须执行的操作,比如网络请求、数据库读写、定时器等。这时候 LaunchedEffect 就派上用场了。
LaunchedEffect 会在 Composable 进入组合时启动一个协程,并在离开组合时自动取消。它的基本用法是这样的:
@Composable
fun UserProfileScreen(userId: String) {
val viewModel: UserViewModel = viewModel()
val userState by viewModel.userState.collectAsState()
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// 根据 userState 渲染 UI
}
注意 LaunchedEffect 的第一个参数是 key。当 key 发生变化时,旧的协程会被取消,新的协程会启动。这有什么用呢?你想想看,如果用户切换了 userId,我们肯定希望重新加载数据,而不是继续用旧数据。这就是 key 的作用。
实战:一个完整的搜索页面
把上面三个知识点串起来,我们做一个搜索页面。用户输入关键词,ViewModel 发起网络请求,结果实时展示。
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
private val _results = MutableStateFlow<List<String>>(emptyList())
val results: StateFlow<List<String>> = _results.asStateFlow()
fun updateQuery(newQuery: String) {
_query.value = newQuery
}
fun search() {
viewModelScope.launch {
// 模拟网络请求
delay(500)
_results.value = listOf("结果1", "结果2", "结果3")
}
}
}
@Composable
fun SearchScreen() {
val viewModel: SearchViewModel = viewModel()
val query by viewModel.query.collectAsState()
val results by viewModel.results.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = query,
onValueChange = { viewModel.updateQuery(it) },
label = { Text("搜索关键词") }
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.search() }) {
Text("搜索")
}
Spacer(modifier = Modifier.height(16.dp))
results.forEach { result ->
Text(result, modifier = Modifier.padding(4.dp))
}
}
}
这个例子虽然简单,但涵盖了 ViewModel、collectAsState、以及 ViewModel 内部的协程使用。实际项目中,你可能会加上防抖、分页、错误处理等逻辑,但核心骨架就是这个。
知识体系总览
下面这张图把 Compose + ViewModel 的核心关系画出来了,方便你理解整体流程:
几个容易忽略的细节
- StateFlow 的初始值: 一定要给初始值,否则
collectAsState()第一次拿到的可能是 null。我习惯在 ViewModel 的 init 块里初始化所有 StateFlow。 - LaunchedEffect 的 key 选择: 如果 key 是固定值(比如
Unit或true),那么 LaunchedEffect 只会执行一次。如果 key 是可变数据,每次变化都会重新执行。别把 key 设成会频繁变化的值,否则协程会反复启动和取消。 - 避免在 ViewModel 里持有 Composable 引用: 有些新手会把 Composable 函数或 Context 传给 ViewModel,这是大忌。ViewModel 应该只处理数据,不关心 UI。
好了,这一章的内容就到这里。记住我说的:ViewModel 是大脑,collectAsState 是神经,LaunchedEffect 是肌肉。把这三者的关系理清了,Compose 开发就会顺畅很多。