Jetpack Compose 入门系列(七):ViewModel 与界面状态管理
学完上篇你已经会用 Navigation 3 在多个页面之间跳转、传参和管理返回栈了。但页面一复杂,只靠
remember把状态写在 Composable 里就不够了,本篇解决一个问题:如何用 ViewModel 管理界面状态,让页面更稳定、更好维护。
一、为什么需要 ViewModel
前面几篇里,我们已经用过 remember 和 rememberSaveable 管理状态,比如输入框内容、按钮点击次数、Tab 选中项。
这种写法在简单页面里没问题:
kotlin
@Composable
fun CounterScreen() {
var count by rememberSaveable { mutableIntStateOf(0) }
Button(
onClick = {
count++
}
) {
Text(text = "点击次数:$count")
}
}
对于这种"只属于当前 UI 的小状态",remember 就够了------计数器用 rememberSaveable 只是顺手让它在旋转屏幕后不归零,不代表所有小状态都要用它。
但真实页面通常没这么简单。比如一个课程列表页,可能有:
- 加载中
- 加载成功
- 加载失败
- 空列表
- 搜索关键词
- 刷新按钮
- 点击重试
如果这些都写在 Composable 里,页面很快就会变成这样:
kotlin
@Composable
fun CourseListScreen() {
// 这些是业务状态,正常应该交给 ViewModel,这里硬塞进 Composable 只是为了演示"反面示例"
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
var courses by remember { mutableStateOf(emptyList<String>()) }
var keyword by remember { mutableStateOf("") }
// 下面继续写 UI、请求、错误处理、刷新逻辑......
}
这里不只是状态多的问题:isLoading、errorMessage、courses 都是业务状态,正常应该交给 ViewModel 管------ViewModel。把它们塞进 Composable 里,加载逻辑和 UI 搅在一起,才是真正的问题。
这就开始不舒服了:Composable 既负责画界面,又负责业务状态,还要处理加载逻辑。时间一长,这个函数就会胖成一坨。
在 XML 时代,我们通常会用 Activity/Fragment + ViewModel:
| XML 时代 | Compose 时代 |
|---|---|
| XML 负责布局 | Composable 负责界面 |
| Activity/Fragment 观察状态 | Composable 收集状态 |
| ViewModel 保存页面数据 | ViewModel 保存页面状态 |
| LiveData 更新 UI | StateFlow 更新 UI |
Compose 并没有取消 ViewModel,反而更适合和 ViewModel 搭配。
用一句话概括:Composable 负责展示,ViewModel 负责状态和逻辑。
二、添加 ViewModel 和 Lifecycle 依赖
这篇会用到三个重点:
| 依赖 | 作用 |
|---|---|
lifecycle-viewmodel-compose |
在 Composable 中获取 ViewModel |
lifecycle-runtime-compose |
提供 collectAsStateWithLifecycle() |
kotlinx-coroutines-android |
在 ViewModel 中使用协程 |
2.1 libs.versions.toml
toml
[versions]
agp = "9.2.1"
kotlin = "2.2.10"
composeBom = "2026.02.01"
activity = "1.8.0"
lifecycle = "2.11.0"
coroutines = "1.11.0"
[libraries]
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
2.2 app/build.gradle.kts
kotlin
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
}
android {
namespace = "com.example.viewmodeldemo"
compileSdk = 36
defaultConfig {
applicationId = "com.example.viewmodeldemo"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.kotlinx.coroutines.android)
}
这段配置里发生了什么:
lifecycle-viewmodel-compose让你可以在 Composable 中使用viewModel()。lifecycle-runtime-compose提供collectAsStateWithLifecycle()。kotlinx-coroutines-android让 ViewModel 能通过viewModelScope启动协程。
关键点:在 Compose 里收集
StateFlow,推荐使用collectAsStateWithLifecycle(),而不是直接用collectAsState()。它会根据页面生命周期自动开始和停止收集,少踩很多坑。
三、UI State:把页面状态收成一个对象
很多刚开始写 ViewModel 的同学,会这样定义状态:
kotlin
class CourseViewModel : ViewModel() {
var isLoading by mutableStateOf(false)
var errorMessage by mutableStateOf<String?>(null)
var courses by mutableStateOf(emptyList<String>())
}
能用,但页面状态一多,很容易散。
更推荐把一个页面的状态收成一个 UiState:
kotlin
data class CourseListUiState(
val keyword: String = "",
val contentState: CourseListContentState = CourseListContentState.Loading
)
sealed interface CourseListContentState {
data object Loading : CourseListContentState
data class Error(
val message: String
) : CourseListContentState
data object Empty : CourseListContentState
data class Success(
val courses: List<Course>
) : CourseListContentState
}
再定义页面数据:
kotlin
data class Course(
val id: Int,
val title: String,
val description: String
)
这段代码里发生了什么:
CourseListUiState表示课程列表页当前所有 UI 状态。keyword表示搜索框输入内容,它是页面里的普通状态。contentState表示内容区域当前处于哪种状态。Loading、Error、Empty、Success是互斥状态,同一时间只能出现一种。Success里携带课程列表,Error里携带错误信息。
为什么要这样写?因为页面里有两类状态:
| 状态类型 | 示例 | 推荐写法 |
|---|---|---|
| 可以和其他状态并存的普通状态 | 搜索关键词、Tab 选中项、开关状态 | 放在 data class UiState 里 |
| 互斥的内容状态 | 加载中、加载失败、空列表、有数据 | 用 sealed interface 表达 |
如果只用 isLoading、errorMessage、courses 这几个字段,也能实现页面,但它允许一些不合理状态,比如"正在加载"同时又"加载失败"。用密封类把内容状态收起来,可以让状态更明确。
页面状态集中以后,Composable 的参数会更清晰:
kotlin
@Composable
private fun CourseListContent(
uiState: CourseListUiState,
onKeywordChange: (String) -> Unit,
onRetryClick: () -> Unit
) {
// 根据 uiState 画界面
}
关键点:一个页面最好对应一个
UiState。普通状态放在data class里,互斥状态可以用密封类表达,别让加载中、错误、数据、输入框状态散落在各处。
四、ViewModel:用 StateFlow 暴露状态
接下来写 ViewModel。
完整代码如下:
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class CourseListViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CourseListUiState())
val uiState: StateFlow<CourseListUiState> = _uiState.asStateFlow()
init {
loadCourses()
}
fun onKeywordChange(keyword: String) {
_uiState.update {
it.copy(keyword = keyword)
}
}
fun retry() {
loadCourses()
}
private fun loadCourses() {
viewModelScope.launch {
_uiState.update {
it.copy(
contentState = CourseListContentState.Loading
)
}
delay(1000)
_uiState.update {
it.copy(
contentState = if (sampleCourses.isEmpty()) {
CourseListContentState.Empty
} else {
CourseListContentState.Success(sampleCourses)
}
)
}
}
}
}
逐步拆解:
_uiState是 ViewModel 内部可修改的状态。uiState是暴露给 UI 层的只读状态。asStateFlow()把MutableStateFlow以只读的StateFlow类型暴露,让外部在编译期就无法调用value =修改,从而划清"谁能改"的边界。init中进入页面后自动加载课程。onKeywordChange()响应搜索框输入。retry()响应重试按钮。viewModelScope.launch用来启动和 ViewModel 生命周期绑定的协程。_uiState.update { it.copy(...) }基于旧状态生成新状态。- 加载中、加载失败、空列表、有数据这些互斥状态通过
CourseListContentState表达。
为什么要分 _uiState 和 uiState?
| 属性 | 谁能改 | 给谁用 |
|---|---|---|
_uiState |
ViewModel 内部 | ViewModel 自己 |
uiState |
外部只能读 | Composable 页面 |
这就像你开店:仓库你自己能改,顾客只能看货架,不能直接冲进仓库改库存。多花一行字的事,能省一堆 bug。
ViewModel 对外暴露只读
StateFlow,内部保留MutableStateFlow。这是状态管理里非常重要的边界。
五、Composable:用 collectAsStateWithLifecycle 收集状态
ViewModel 有了,页面怎么拿状态?
在 Compose 中可以这样写:
kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun CourseListRoute(
viewModel: CourseListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
CourseListScreen(
uiState = uiState,
onKeywordChange = viewModel::onKeywordChange,
onRetryClick = viewModel::retry
)
}
这段代码里发生了什么:
viewModel()获取当前页面对应的 ViewModel。collectAsStateWithLifecycle()把StateFlow转成 Compose 能观察的State。val uiState by ...让我们可以直接使用uiState。- 状态变化后,Compose 会自动重组。
- 页面事件通过
viewModel::onKeywordChange、viewModel::retry传给 ViewModel。
这里我特意把函数命名为 CourseListRoute,而不是直接叫 CourseListScreen。
通常可以这样拆:
| 层级 | 职责 |
|---|---|
CourseListRoute |
拿 ViewModel、收集状态、连接事件 |
CourseListScreen |
纯 UI,根据参数显示界面 |
CourseListViewModel |
管理状态和业务逻辑 |
结构图如下:
关键点:Route 负责连接 ViewModel 和 UI,Screen 尽量保持"傻瓜式"------给什么状态就显示什么,点了什么就抛事件。
六、根据 UI State 显示不同界面
现在我们写纯 UI 层。
一个列表页一般有四种状态:
| 状态 | 显示内容 |
|---|---|
| 加载中 | Loading 文案 |
| 加载失败 | 错误信息 + 重试按钮 |
| 空列表 | 空状态文案 |
| 有数据 | 列表内容 |
代码如下:
kotlin
@Composable
private fun CourseListScreen(
uiState: CourseListUiState,
onKeywordChange: (String) -> Unit,
onRetryClick: () -> Unit
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "课程列表",
style = MaterialTheme.typography.headlineMedium
)
OutlinedTextField(
value = uiState.keyword,
onValueChange = onKeywordChange,
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "搜索课程")
}
)
when (val contentState = uiState.contentState) {
CourseListContentState.Loading -> {
Text(text = "正在加载课程......")
}
is CourseListContentState.Error -> {
ErrorContent(
message = contentState.message,
onRetryClick = onRetryClick
)
}
CourseListContentState.Empty -> {
Text(text = "暂无课程")
}
is CourseListContentState.Success -> {
val filteredCourses = contentState.courses.filter {
it.title.contains(uiState.keyword, ignoreCase = true)
}
if (filteredCourses.isEmpty()) {
Text(text = "没有找到相关课程")
} else {
filteredCourses.forEach { course ->
CourseItem(course = course)
}
}
}
}
}
}
}
错误区域和列表项拆成单独组件:
kotlin
@Composable
private fun ErrorContent(
message: String,
onRetryClick: () -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = message)
Button(
onClick = onRetryClick
) {
Text(text = "重试")
}
}
}
@Composable
private fun CourseItem(
course: Course
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = course.title,
style = MaterialTheme.typography.titleMedium
)
Text(text = course.description)
}
}
}
这段 UI 代码的重点是:它不关心数据怎么来的。
它只关心:
uiState.contentState当前是Loading、Error、Empty还是Success- 如果是
Success,取出里面的课程列表 uiState.keyword当前是什么搜索词,并用它过滤课程列表- 用户输入时调用
onKeywordChange - 用户点重试时调用
onRetryClick
UI 层不要自己发请求、不要自己改业务状态。它只根据状态画界面,把用户操作告诉外层。
七、常见错误:不要让状态到处乱飞
7.1 不要在 Composable 里直接写加载逻辑
❌ 错误:
kotlin
@Composable
fun CourseListScreen() {
var courses by remember { mutableStateOf(emptyList<Course>()) }
LaunchedEffect(Unit) {
courses = loadCoursesFromNetwork()
}
// 显示课程列表
}
这段代码的问题是:页面既负责 UI,又负责加载数据。以后要加重试、错误、刷新、缓存,很快就会乱。
✅ 正确:
kotlin
@Composable
fun CourseListRoute(
viewModel: CourseListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
CourseListScreen(
uiState = uiState,
onKeywordChange = viewModel::onKeywordChange,
onRetryClick = viewModel::retry
)
}
加载逻辑放 ViewModel,UI 只收状态。
LaunchedEffect不是不能用,但不要把整页业务加载逻辑都塞进 Composable。
7.2 不要把 MutableStateFlow 暴露给 UI
❌ 错误:
kotlin
class CourseListViewModel : ViewModel() {
val uiState = MutableStateFlow(CourseListUiState())
}
这样 UI 层拿到后,也可以直接改:
kotlin
viewModel.uiState.value = CourseListUiState()
边界就乱了。
✅ 正确:
kotlin
class CourseListViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CourseListUiState())
val uiState: StateFlow<CourseListUiState> = _uiState.asStateFlow()
}
ViewModel 内部能改,UI 外部只能读。
7.3 不要用 collectAsState 替代 collectAsStateWithLifecycle
❌ 错误:
kotlin
val uiState by viewModel.uiState.collectAsState()
✅ 正确:
kotlin
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
collectAsStateWithLifecycle() 会结合生命周期收集 Flow,更适合 Android 页面。
在 Android Compose 项目里,只要是从 ViewModel 收集
StateFlow,优先使用collectAsStateWithLifecycle()。
八、综合实战:课程列表状态管理 Demo
下面把前面的内容串起来,写一个完整可运行的 Demo:
- 进入页面自动加载课程
- 显示加载中
- 加载完成后展示课程列表
- 支持搜索过滤
- 支持模拟错误和重试
- 支持模拟空列表,让
Empty状态也能被触发 - UI 只根据
UiState和ContentState显示内容 - ViewModel 负责状态和逻辑
完整代码如下:
kotlin
package com.example.viewmodeldemo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
CourseListRoute()
}
}
}
}
data class Course(
val id: Int,
val title: String,
val description: String
)
data class CourseListUiState(
val keyword: String = "",
val contentState: CourseListContentState = CourseListContentState.Loading
)
sealed interface CourseListContentState {
data object Loading : CourseListContentState
data class Error(
val message: String
) : CourseListContentState
data object Empty : CourseListContentState
data class Success(
val courses: List<Course>
) : CourseListContentState
}
class CourseListViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CourseListUiState())
val uiState: StateFlow<CourseListUiState> = _uiState.asStateFlow()
private var shouldFailNextTime = false
private var shouldEmptyNextTime = false
// 保存上一次加载任务,新请求前取消旧的,避免快速连点时多个协程叠加
private var loadJob: Job? = null
init {
loadCourses()
}
fun onKeywordChange(keyword: String) {
_uiState.update {
it.copy(keyword = keyword)
}
}
fun retry() {
loadCourses()
}
fun simulateError() {
shouldFailNextTime = true
loadCourses()
}
fun simulateEmpty() {
shouldEmptyNextTime = true
loadCourses()
}
private fun loadCourses() {
loadJob?.cancel()
loadJob = viewModelScope.launch {
_uiState.update {
it.copy(
contentState = CourseListContentState.Loading
)
}
delay(1000)
when {
shouldFailNextTime -> {
shouldFailNextTime = false
_uiState.update {
it.copy(
contentState = CourseListContentState.Error(
message = "课程加载失败,请稍后重试"
)
)
}
}
shouldEmptyNextTime -> {
shouldEmptyNextTime = false
_uiState.update {
it.copy(
contentState = CourseListContentState.Empty
)
}
}
else -> {
_uiState.update {
it.copy(
contentState = if (sampleCourses.isEmpty()) {
CourseListContentState.Empty
} else {
CourseListContentState.Success(sampleCourses)
}
)
}
}
}
}
}
}
private val sampleCourses = listOf(
Course(
id = 1,
title = "Compose 基础控件",
description = "学习 Text、Button、TextField 等常用组件。"
),
Course(
id = 2,
title = "Compose 布局与 State",
description = "掌握 Column、Row、Box 和状态管理基础。"
),
Course(
id = 3,
title = "Navigation 3 页面导航",
description = "学习页面跳转、传参和返回栈管理。"
),
Course(
id = 4,
title = "ViewModel 状态管理",
description = "把页面状态从 Composable 中拆出来。"
)
)
@Composable
private fun CourseListRoute(
viewModel: CourseListViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
CourseListScreen(
uiState = uiState,
onKeywordChange = viewModel::onKeywordChange,
onRetryClick = viewModel::retry,
onSimulateErrorClick = viewModel::simulateError,
onSimulateEmptyClick = viewModel::simulateEmpty
)
}
@Composable
private fun CourseListScreen(
uiState: CourseListUiState,
onKeywordChange: (String) -> Unit,
onRetryClick: () -> Unit,
onSimulateErrorClick: () -> Unit,
onSimulateEmptyClick: () -> Unit
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "课程列表",
style = MaterialTheme.typography.headlineMedium
)
OutlinedTextField(
value = uiState.keyword,
onValueChange = onKeywordChange,
modifier = Modifier.fillMaxWidth(),
label = {
Text(text = "搜索课程")
}
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = onRetryClick
) {
Text(text = "刷新")
}
OutlinedButton(
onClick = onSimulateErrorClick
) {
Text(text = "模拟错误")
}
OutlinedButton(
onClick = onSimulateEmptyClick
) {
Text(text = "模拟空列表")
}
}
CourseListStateContent(
uiState = uiState,
onRetryClick = onRetryClick
)
}
}
}
@Composable
private fun CourseListStateContent(
uiState: CourseListUiState,
onRetryClick: () -> Unit
) {
when (val contentState = uiState.contentState) {
CourseListContentState.Loading -> {
Text(text = "正在加载课程......")
}
is CourseListContentState.Error -> {
ErrorContent(
message = contentState.message,
onRetryClick = onRetryClick
)
}
CourseListContentState.Empty -> {
Text(text = "暂无课程")
}
is CourseListContentState.Success -> {
val filteredCourses = contentState.courses.filter {
it.title.contains(uiState.keyword, ignoreCase = true)
}
if (filteredCourses.isEmpty()) {
Text(text = "没有找到相关课程")
} else {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
filteredCourses.forEach { course ->
CourseItem(course = course)
}
}
}
}
}
}
@Composable
private fun ErrorContent(
message: String,
onRetryClick: () -> Unit
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = message)
Button(
onClick = onRetryClick
) {
Text(text = "重试")
}
}
}
@Composable
private fun CourseItem(
course: Course
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = course.title,
style = MaterialTheme.typography.titleMedium
)
Text(text = course.description)
}
}
}
这份 Demo 的数据流如下:
实战知识点对应表
| 实战中的效果 | 使用的 API | 对应章节 |
|---|---|---|
| 页面状态集中管理 | CourseListUiState |
三 |
| ViewModel 保存状态 | CourseListViewModel |
四 |
| 对外暴露只读状态 | StateFlow + asStateFlow() |
四 |
| 修改状态 | _uiState.update { it.copy(...) } |
四 |
| 页面收集状态 | collectAsStateWithLifecycle() |
五 |
| Route 连接状态与 UI | CourseListRoute |
五 |
| 根据状态显示不同内容 | when 判断 contentState |
六 |
| 加载、错误、空状态、列表 | CourseListContentState |
六、八 |
这就是 Compose 里常见的一条线:用户操作 → ViewModel 改状态 → StateFlow 发出新状态 → Composable 重组。
九、ViewModel 使用速查表
| 你想做什么 | 推荐写法 |
|---|---|
| 定义页面状态 | data class XxxUiState(...) + sealed interface XxxContentState |
| 创建 ViewModel | class XxxViewModel : ViewModel() |
| 内部可变状态 | private val _uiState = MutableStateFlow(...) |
| 对外只读状态 | val uiState = _uiState.asStateFlow() |
| 修改状态 | _uiState.update { it.copy(...) } |
| 启动异步任务 | viewModelScope.launch { ... } |
| Composable 获取 ViewModel | viewModel: XxxViewModel = viewModel() |
| 收集 StateFlow | collectAsStateWithLifecycle() |
| UI 组件暴露事件 | onClick、onRetryClick、onKeywordChange |
十、总结
本篇你学到了 Compose 中 ViewModel 与界面状态管理的核心写法:
- 状态分层 :
remember适合组件内部小状态,ViewModel 适合页面级业务状态 - UI State :用
data class XxxUiState描述页面普通状态,用密封类描述互斥的内容状态 - StateFlow :ViewModel 内部用
MutableStateFlow,对外暴露只读StateFlow - 状态更新 :用
update { it.copy(...) }基于旧状态生成新状态 - 生命周期收集 :Composable 中使用
collectAsStateWithLifecycle()收集状态 - Route 与 Screen 拆分:Route 负责连接 ViewModel,Screen 负责纯 UI 展示
- 状态驱动 UI :根据
Loading、Error、Empty、Success等状态显示不同界面
核心原则:Composable 不应该变成业务逻辑大杂烩,页面状态和业务操作交给 ViewModel,UI 只负责根据状态显示结果。
下一篇我们将学习 Compose 中的副作用处理 ------如何正确使用 LaunchedEffect、DisposableEffect、rememberUpdatedState,处理一次性任务、生命周期回调和状态变化触发的逻辑。
如果你在学习过程中有任何疑问,欢迎在评论区留言,我会尽可能回复。
系列文章: