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 里最常用的数据源就是 StateFlowLiveData。在 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 就会自动更新。

小技巧: 如果你用的是 LiveData,可以用 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 的作用。

我曾经踩过的坑: 在 LaunchedEffect 里直接调用 viewModel 的 suspend 函数时,如果 ViewModel 的协程作用域和 LaunchedEffect 的协程作用域不一致,可能会导致内存泄漏或重复请求。我的建议是:在 ViewModel 里用 viewModelScope 启动协程,LaunchedEffect 只负责触发,不负责执行耗时任务。

实战:一个完整的搜索页面

把上面三个知识点串起来,我们做一个搜索页面。用户输入关键词,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 的核心关系画出来了,方便你理解整体流程:

Compose + ViewModel 核心数据流 ViewModel StateFlow<T> viewModelScope 业务逻辑 / 网络请求 collectAsState() Flow → State 自动收集 & 取消 生命周期感知 Compose UI State 驱动重组 LaunchedEffect 用户交互事件 StateFlow State 用户操作事件(点击、输入等) LaunchedEffect 协程副作用管理 数据流方向:ViewModel → collectAsState → Compose UI → 用户事件 → ViewModel

几个容易忽略的细节

  • StateFlow 的初始值: 一定要给初始值,否则 collectAsState() 第一次拿到的可能是 null。我习惯在 ViewModel 的 init 块里初始化所有 StateFlow。
  • LaunchedEffect 的 key 选择: 如果 key 是固定值(比如 Unittrue),那么 LaunchedEffect 只会执行一次。如果 key 是可变数据,每次变化都会重新执行。别把 key 设成会频繁变化的值,否则协程会反复启动和取消。
  • 避免在 ViewModel 里持有 Composable 引用: 有些新手会把 Composable 函数或 Context 传给 ViewModel,这是大忌。ViewModel 应该只处理数据,不关心 UI。
核心总结: ViewModel 负责数据和业务逻辑,collectAsState 负责把数据流变成 UI 可观察的状态,LaunchedEffect 负责管理协程副作用。这三者配合,基本能覆盖 90% 的 Compose 页面开发场景。

好了,这一章的内容就到这里。记住我说的:ViewModel 是大脑,collectAsState 是神经,LaunchedEffect 是肌肉。把这三者的关系理清了,Compose 开发就会顺畅很多。

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