Jetpack Compose中viewModel到Compose数据传递及最佳实践

前言

使用 ViewModel我们可以方便管理模型数据的生命周期。

使用方式参考

https://www.psvmc.cn/article/2025-08-12-jetpack-compose-comp-viewmodel.html

ViewModel 与 Compose 的数据交互本质是:

ViewModel 用可观察容器(StateFlow/LiveData)持有数据 → Compose 通过 collectAsState/observeAsState 观察状态 → 数据变化时触发 UI 重组

ViewModel中的数据怎样供Compose使用呢?

两种实现方式

使用MutableState

优点是使用起来简单,但是从架构设计上来说是不太合理的。

使用没有问题,并不是不能用,只是不推荐使用。

ViewModel 中使用 mutableStateOf

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

class MyViewModel : ViewModel() {
var count by mutableStateOf(0)
private set // 尝试限制外部修改,但仍暴露了 MutableState 的特性

fun increment() {
count++
}
}

Composable

1
2
3
4
5
6
7
8
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// 直接观察 ViewModel 中的 MutableState
Text("Count: ${viewModel.count}")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}

这种方式可以使用,并不会引起内存泄露。

  • Compose 会自动跟踪哪些 UI 元素依赖了 State 对象。当 State 的值发生变化时,Compose 只会重组依赖它的那部分 UI,而不是整个屏幕。
  • 更重要的是,当一个 Composable 函数不再被使用(例如,用户导航到其他页面),Compose 会自动解除它与所有 State 对象的关联,从而避免了因 State 引用导致的内存泄漏。

使用StateFlow

推荐的方式

ViewModel 中使用 StateFlow

基本数据

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class MyViewModel : ViewModel() {
// 私有的 MutableStateFlow,用于在 ViewModel 内部更新
private val _count = MutableStateFlow(0)

// 公开的 StateFlow,用于向 UI 暴露状态
val count: StateFlow<Int> = _count

fun increment() {
// 可以在任何线程安全地更新
_count.value++
}

// 示例:模拟一个耗时操作后更新状态
fun fetchData() {
viewModelScope.launch {
// 模拟网络请求或数据库查询
val result = someExpensiveOperation()
_count.value = result
}
}
}

Composable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
// 使用 collectAsStateWithLifecycle 来观察 StateFlow
// 这是生命周期安全的,当屏幕不可见时会暂停收集
val count by viewModel.count.collectAsStateWithLifecycle()

Text("Count: $count")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
Button(onClick = { viewModel.fetchData() }) {
Text("Fetch Data")
}
}

列表数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 数据类
data class Todo(val id: Int, val title: String, val isCompleted: Boolean = false)

// ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class TodoViewModel : ViewModel() {
// _todos 是私有的,只能在 ViewModel 内部修改
private val _todos = MutableStateFlow<List<Todo>>(emptyList())

// todos 是公开的,暴露给 UI 层观察,外部无法直接修改
val todos: StateFlow<List<Todo>> = _todos.asStateFlow()

init {
// 模拟从数据库或网络加载初始数据
loadInitialData()
}

private fun loadInitialData() {
viewModelScope.launch {
// 模拟网络延迟
kotlinx.coroutines.delay(1000)
val initialTodos = listOf(
Todo(1, "学习 Jetpack Compose"),
Todo(2, "掌握 StateFlow"),
Todo(3, "写一个示例 App")
)
_todos.value = initialTodos
}
}

// 添加新的待办事项
fun addTodo(title: String) {
val newTodo = Todo(
id = (_todos.value.maxByOrNull { it.id }?.id ?: 0) + 1,
title = title
)
// 重要:StateFlow 的 value 更新必须是一个新的对象,Compose 才能感知到变化
_todos.value = _todos.value + newTodo
}

// 切换待办事项的完成状态
fun toggleTodoCompletion(id: Int) {
_todos.value = _todos.value.map { todo ->
if (todo.id == id) {
todo.copy(isCompleted = !todo.isCompleted)
} else {
todo
}
}
}

// 删除待办事项
fun deleteTodo(id: Int) {
_todos.value = _todos.value.filterNot { it.id == id }
}
}

总结

在 ViewModel 中直接使用 mutableStateOf 对于极其简单的场景可能看起来很方便,但它带来了架构耦合功能受限的问题。

最佳实践是:

  • ViewModel 应该暴露 StateFlow:这符合分层架构原则,使 ViewModel 更加健壮、可测试和可复用。
  • Composable 应该观察 StateFlow:使用 collectAsStateWithLifecycle 可以确保状态收集是生命周期安全的,避免内存泄漏和不必要的资源消耗。

因此,在 ViewModel 中直接使用 mutableStateOf 通常是不合理的,推荐使用 StateFlow 作为 ViewModel 向 UI 暴露状态的首选方式。

最佳实现

最佳的方式是使用StateFlow结合自定义状态类。但是使用起来相对也麻烦点。

定义状态类

1
2
3
4
5
6
7
8
9
// 封装页面所需的所有状态
data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val errorMsg: String? = null
)

// 数据模型
data class User(val name: String, val age: Int)

ViewModel

ViewModel 管理复合状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UserUiState(isLoading = true))
val uiState = _uiState.asStateFlow()

// 模拟获取用户数据(包含加载、成功、失败状态)
fun loadUser() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true) // 加载中
try {
delay(1500)
val mockUser = User("张三", 28)
_uiState.value = UserUiState(user = mockUser) // 成功
} catch (e: Exception) {
_uiState.value = UserUiState(errorMsg = "加载失败:${e.message}") // 失败
}
}
}
}

Compose

Compose 根据状态展示 UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composable
fun UserScreen() {
val vm: UserViewModel = viewModels()
val uiState = vm.uiState.collectAsState(initial = UserUiState()).value

when {
uiState.isLoading -> Text("加载中...")
uiState.errorMsg != null -> Text("错误:${uiState.errorMsg}")
uiState.user != null -> Text("用户:${uiState.user.name},年龄:${uiState.user.age}")
}

// 触发加载
Text(
text = "点击加载用户",
modifier = Modifier.clickable { viewModel.loadUser() }
)
}

常见问题

为什么适合用一个数据类封装状态?

符合 “单一可信源” 原则

页面所有状态(加载中、数据、错误、用户输入等)集中在一个数据类中,避免状态分散在 ViewModel 的多个变量中(如 isLoading: Boolean、data: List<Item>、error: String? 分别定义),确保状态间的一致性。
例如:加载中数据就绪 是互斥状态,用一个类封装可通过逻辑保证不会同时为 true:

1
2
3
4
5
6
7
8
9
10
// 集中管理的状态类
data class PageUiState(
val isLoading: Boolean = false,
val data: List<String>? = null,
val error: String? = null
)

// ViewModel 中更新状态时,天然保证互斥性
_uiState.value = PageUiState(isLoading = true) // 加载时其他状态自动清空
_uiState.value = PageUiState(data = listOf("A", "B")) // 数据就绪时加载和错误状态自动清空

简化状态传递与观察

Compose 中只需观察一个状态对象(如 uiState.collectAsState()),而非多个独立状态(isLoading.collectAsState()、data.collectAsState() 等),减少模板代码。
对比两种方式:

1
2
3
4
5
6
7
8
// 方式1:分散状态(繁琐)
val isLoading by viewModel.isLoading.collectAsState()
val data by viewModel.data.collectAsState()
val error by viewModel.error.collectAsState()

// 方式2:单一状态类(简洁)
val uiState by viewModel.uiState.collectAsState()
// 使用时直接访问 uiState.isLoading、uiState.data 等

便于状态回溯与调试

每个状态变更都是一个完整的数据类实例,可通过日志记录完整状态快照(如 Log.d(“State”, “Updated: $uiState”)),轻松追踪状态流转过程。

适配 Compose 重组特性

Compose 会根据状态变化智能重组,单一状态类的属性变更只会触发依赖该属性的 UI 部分重组(而非整个页面),性能不受影响。

对象变化,对象中未改变的值会触发组件重组吗?

在 Jetpack Compose 中,状态类中部分值变化时,未变化的字段不会触发依赖该字段的 UI 部分重新渲染

Compose 会通过 “智能重组” 机制,仅更新真正依赖变化数据的 UI 组件,其他部分保持不变。

ViewModel中为什么MutableStateFlow还要转成StateFlow?

MutableStateFlow 提供了 value 的 setter 方法(可以直接修改值),而通过 asStateFlow() 转换为 StateFlow 后,得到的是一个只读视图
外部只能观察它的值(通过 collect),但不能直接修改它,确保状态只能在合适的范围内被修改(通常是创建它的类内部)。

通过暴露 StateFlow 而非 MutableStateFlow,向调用者清晰传达了 这是一个状态数据流,你应该观察它而不是修改它 的意图,让代码职责更明确。

为什么ViewModel 中直接使用 Compose 状态不合理?

在 ViewModel 中直接使用 Compose 状态(如 mutableStateOf不合理

主要违反了架构设计原则和分层职责,具体原因如下:

  1. 职责边界模糊

ViewModel 的核心职责是:

  • 持有与 UI 相关的数据,且生命周期长于 UI(不受配置变更影响)
  • 处理业务逻辑,暴露数据给 UI 层
  • 不依赖 UI 框架(如 Compose、Activity 等)

而 Compose 状态(mutableStateOf 等)是 UI 层的状态持有工具,其设计目的是为了驱动 Composable 重组,属于 UI 框架的一部分。
将 Compose 状态放入 ViewModel,会导致 ViewModel 依赖 UI 框架,违反了 “ViewModel 应与 UI 框架解耦” 的设计原则。

  1. 生命周期不匹配
  • Compose 状态的生命周期与 Composable 组件绑定,会随组件的创建 / 销毁而变化。
  • ViewModel 的生命周期与 Activity/Fragment 一致。
  1. 架构扩展性问题
  • 若未来 UI 层不使用 Compose(如迁移到其他框架),ViewModel 中的 Compose 状态会成为强耦合点,增加重构成本。
  • Compose 状态的设计初衷是局部 UI 状态管理,不适合作为跨层(数据层→UI 层)的数据传递载体,缺乏 StateFlow 等数据流的背压处理、线程安全等特性。

ViewModel 的核心是 “管理数据,解耦 UI”,而 Compose 状态是 “驱动 UI 重组的工具”。二者属于不同层级,强行在 ViewModel 中使用 Compose 状态会破坏架构分层,导致维护性和扩展性问题。
正确的模式是:ViewModel 用 StateFlow 管理状态,UI 层通过 collectAsState() 桥接为 Compose 状态,清晰分离职责。

代码组织方式

方式 优点 缺点 适用场景
分层架构 职责清晰,耦合度低,可测试性高,易于扩展 对于非常小的项目可能显得有些繁琐 中大型项目,需要清晰架构和长期维护的项目
功能模块 内聚性高,查找方便 模糊了分层界限,可能导致 ViewModel 依赖 UI 小型项目团队偏好按功能模块组织代码