Jetpack Compose 统一状态管理 —— 从 ViewModel 到跨组件共享

本文涵盖 Compose 状态管理的全套方案:状态提升、ViewModel + StateFlow、CompositionLocal、SavedStateHandle、以及复杂场景下的架构选型。配合 Compose 四大核心概念 中的 State 章节阅读效果最佳。

状态管理全景图

本地状态(组件内部)
  mutableStateOf + remember
    ↓ 提升
状态提升(父子传参)
  状态在父组件,通过参数下发
    ↓ 跨页面
ViewModel(页面级)
  StateFlow + collectAsState,生命周期感知
    ↓ 跨组件
CompositionLocal(作用域共享)
  Theme、Context 等隐式传递
    ↓ 跨应用
持久化状态
  rememberSaveable / DataStore / Room
层级方案存活范围适用场景
组件内部remember + mutableStateOf组件在组合树中开关、展开/折叠、输入框
父子通信状态提升 (State Hoisting)父组件生命周期表单、列表项
页面级ViewModel + StateFlowActivity/Fragment 生命周期页面数据、网络请求结果
作用域共享CompositionLocalComposable 子树主题、导航栈、依赖注入
持久化rememberSaveable / DataStore进程存活 / 持久文件配置变更保留 / 设置、Token
全局单例 + StateFlowApplication用户信息、购物车

状态提升(State Hoisting)—— 最基本的心智模型

什么是状态提升

将状态从子组件「提升」到最近的共同父组件,子组件通过参数接收状态和事件回调。

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
// ❌ 状态下沉:子组件自己管状态 → 父组件无法控制
@Composable
fun SearchField_Bad() {
var text by remember { mutableStateOf("") } // 锁死在内部
TextField(value = text, onValueChange = { text = it })
}

// ✅ 状态提升:父组件持有状态,子组件只负责展示
@Composable
fun SearchField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
placeholder = { Text("搜索…") }
)
}

// 父组件
@Composable
fun HomeScreen() {
var query by remember { mutableStateOf("") }

Column {
SearchField(value = query, onValueChange = { query = it })
// 父组件可以读取 query 做搜索 → 状态源唯一
Text("搜索:$query")
}
}

状态提升三原则

原则说明
单一数据源状态只有一个持有者,避免多处各自维护导致不一致
单向数据流状态向下传递(参数),事件向上冒泡(回调)
不可变性参数用 val / data class,接收方不修改传入的状态

提升到什么层级?

1
2
3
4
① 兄弟组件间共享       → 提升到共同父组件
② 多页面间共享 → 提升到 ViewModel
③ 整个子树隐式共享 → 用 CompositionLocal
④ 全局(登录态等) → 单例 + StateFlow

🔑 状态提升是 Compose 的通用模式,几乎所有官方 Material 组件都遵循此模式(如 TextFieldCheckboxSwitch)。

ViewModel + StateFlow —— 页面级状态管理(推荐)

为什么需要 ViewModel

remember 只能在 Composable 中存活。一旦页面被销毁重建(屏幕旋转、进程重启),状态就丢了。ViewModel 绑定到 Activity/Fragment 的生命周期,可以在配置变更后存活

1
2
3
// build.gradle.kts
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")

标准模板

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
63
64
65
// ① 定义 UI 状态 —— 用一个 data class 聚合所有状态
data class HomeUiState(
val isLoading: Boolean = true,
val items: List<String> = emptyList(),
val errorMessage: String? = null,
val searchQuery: String = ""
)

// ② ViewModel —— 用 MutableStateFlow 管理状态
class HomeViewModel : ViewModel() {

private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

init {
loadData()
}

fun onSearchQueryChange(query: String) {
_uiState.update { it.copy(searchQuery = query) }
}

fun refresh() {
_uiState.update { it.copy(isLoading = true) }
loadData()
}

private fun loadData() {
viewModelScope.launch {
try {
// 模拟网络请求
val data = fetchItems()
_uiState.update { it.copy(
isLoading = false,
items = data,
errorMessage = null
)}
} catch (e: Exception) {
_uiState.update { it.copy(
isLoading = false,
errorMessage = e.message
)}
}
}
}
}

// ③ Composable —— collectAsState 收集状态
@Composable
fun HomeScreen(viewModel: HomeViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()

when {
uiState.isLoading -> LoadingIndicator()
uiState.errorMessage != null -> ErrorView(uiState.errorMessage!!) {
viewModel.refresh()
}
else -> ContentList(
items = uiState.items,
query = uiState.searchQuery,
onQueryChange = viewModel::onSearchQueryChange,
onRefresh = viewModel::refresh
)
}
}

为什么用 StateFlow 而不是 LiveData

对比StateFlowLiveData
类型安全✅ 编译时检查⚠️ 运行时检查
初始值✅ 必须有初始值❌ 可选
线程✅ 不限线程⚠️ 仅主线程 setValue
CollectcollectAsState()observeAsState()
Flow 操作符mapcombinefilter❌ 需要 Transformations
测试✅ 直接 runTest collect⚠️ 需要 InstantTaskExecutorRule

Google 官方推荐 StateFlow 作为 Compose 状态容器的首选

单状态对象 vs 多状态对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ❌ 分散管理 —— 难以追踪状态,容易漏更新
class BadViewModel : ViewModel() {
val isLoading = MutableStateFlow(false)
val items = MutableStateFlow<List<String>>(emptyList())
val error = MutableStateFlow<String?>(null)
// 每次都要单独更新,UI 要收集 3 个 Flow
}

// ✅ 单一 data class —— 原子更新,UI 只需收集一个 Flow
data class HomeUiState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String? = null
)

class GoodViewModel : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()

// copy() 保证原子更新,不会出现中间态
fun onLoaded(data: List<String>) {
_uiState.update { it.copy(isLoading = false, items = data, error = null) }
}
}

🔑 一条原则:一个 ViewModel → 一个 data class UiState → 一个 StateFlow

SavedStateHandle —— 进程被杀死后恢复

ViewModel 可以存活配置变更但无法存活进程被杀。SavedStateHandle 解决了这个问题:

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
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {

// ① 读写简单键值对(自动持久化到 Bundle)
var query by mutableStateOf(
savedStateHandle.get<String>("query") ?: ""
)
private set

fun onQueryChange(newQuery: String) {
query = newQuery
savedStateHandle["query"] = newQuery // ← 自动持久化
}

// ② StateFlow 方式
val selectedTab = savedStateHandle.getStateFlow("tab", 0)

// ③ 复杂对象(需要序列化)
fun saveForm(form: FormData) {
savedStateHandle["form_json"] = Json.encodeToString(FormData.serializer(), form)
}
fun restoreForm(): FormData {
val json = savedStateHandle.get<String>("form_json") ?: return FormData()
return Json.decodeFromString(FormData.serializer(), json)
}
}
场景方案
配置变更(旋转屏幕)ViewModel(默认)
进程被杀死后恢复SavedStateHandle
跨进程 / 长期保存DataStore / Room

CompositionLocal —— 隐式跨组件共享

当状态需要在整个子树中共享,但又不想逐层手动传递时,使用 CompositionLocal

典型场景

  • MaterialTheme.colors 统一颜色
  • LocalContext.current 获取 Context
  • 自定义主题、语言、导航栈

自定义 CompositionLocal

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
// ① 定义
data class AppTheme(
val isDark: Boolean = false,
val accentColor: Color = Color.Blue
)

val LocalAppTheme = staticCompositionLocalOf { AppTheme() }

// ② 提供
@Composable
fun AppThemeProvider(
isDark: Boolean,
content: @Composable () -> Unit
) {
val theme = if (isDark) {
AppTheme(isDark = true, accentColor = Color(0xFFBB86FC))
} else {
AppTheme()
}
CompositionLocalProvider(LocalAppTheme provides theme) {
content()
}
}

// ③ 消费
@Composable
fun ThemedButton() {
val theme = LocalAppTheme.current
Button(
colors = ButtonDefaults.buttonColors(
containerColor = theme.accentColor
)
) {
Text("按钮")
}
}

compositionLocalOf vs staticCompositionLocalOf

compositionLocalOfstaticCompositionLocalOf
重组范围仅用到 .current 的组件所有组件
性能值变化频繁时更优值几乎不变时更优
典型用途动态主题色、滚动位置Context、主题类型、导航控制器

⚠️ 不要用 CompositionLocal 替代参数传递。只有当跨越多层、逐层传参显得冗余时才使用。

复杂状态管理方案对比

当应用状态越来越复杂(多个 ViewModel 间共享数据、缓存、跨页面通信),可以考虑更结构化的方案:

方案特点适合
ViewModel + StateFlow官方原生,零额外依赖⭐ 中小型项目首选
MVI(Model-View-Intent)单向数据流,Action → State → UI复杂 UI,需要严格状态管理
unidirectional data flow (UDF)Google 官方推荐架构配合 ViewModel 的自然延伸
MoleculeCompose 运行时驱动状态流复杂业务逻辑用 Compose 表达
Store(Circuit/Mobius)状态机驱动,支持 effect 中间件大型应用,团队协作

MVI 核心结构

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
// ① 状态 —— 不可变 data class
data class CounterState(val count: Int = 0, val isLoading: Boolean = false)

// ② 事件 / Intent —— 用户操作
sealed interface CounterEvent {
data object Increment : CounterEvent
data object Decrement : CounterEvent
data object Reset : CounterEvent
data class AsyncIncrement(val after: Long) : CounterEvent
}

// ③ ViewModel —— 处理事件,产生新状态
class CounterViewModel : ViewModel() {
private val _state = MutableStateFlow(CounterState())
val state: StateFlow<CounterState> = _state.asStateFlow()

fun onEvent(event: CounterEvent) {
when (event) {
CounterEvent.Increment -> _state.update { it.copy(count = it.count + 1) }
CounterEvent.Decrement -> _state.update { it.copy(count = it.count - 1) }
CounterEvent.Reset -> _state.update { it.copy(count = 0) }
// 异步操作
is CounterEvent.AsyncIncrement -> {
_state.update { it.copy(isLoading = true) }
viewModelScope.launch {
delay(event.after)
_state.update { it.copy(count = it.count + 1, isLoading = false) }
}
}
}
}
}

// ④ UI —— 发送事件,展示状态
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.state.collectAsState()

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("计数:${state.count}", style = MaterialTheme.typography.headlineMedium)

if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.padding(8.dp))
}

Row {
Button(onClick = { viewModel.onEvent(CounterEvent.Increment) }) { Text("+1") }
Button(onClick = { viewModel.onEvent(CounterEvent.Decrement) }) { Text("-1") }
Button(onClick = { viewModel.onEvent(CounterEvent.Reset) }) { Text("重置") }
}

Button(onClick = {
viewModel.onEvent(CounterEvent.AsyncIncrement(2000))
}) {
Text("2秒后 +1")
}
}
}

MVI 的优点

优点说明
可追溯每个状态变更都源自一个 Event,方便调试和回放
可测试ViewModel 是纯函数:(State, Event) → State
线程安全MutableStateFlow.update {} 原子操作
单向数据流UI → Event → ViewModel → State → UI

全局状态 —— 跨 Activity 共享

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
// 单例 + StateFlow 实现全局购物车
object CartManager {
private val _items = MutableStateFlow<List<CartItem>>(emptyList())
val items: StateFlow<List<CartItem>> = _items.asStateFlow()

val totalCount: StateFlow<Int> = _items
.map { it.sumOf { item -> item.quantity } }
.stateIn(GlobalScope, SharingStarted.WhileSubscribed(), 0)

val totalPrice: StateFlow<Double> = _items
.map { it.sumOf { item -> item.price * item.quantity } }
.stateIn(GlobalScope, SharingStarted.WhileSubscribed(), 0.0)

fun addItem(item: CartItem) {
_items.update { list ->
val idx = list.indexOfFirst { it.id == item.id }
if (idx >= 0) {
list.toMutableList().also {
it[idx] = it[idx].copy(quantity = it[idx].quantity + 1)
}
} else {
list + item
}
}
}

fun removeItem(id: String) {
_items.update { it.filter { item -> item.id != id } }
}

fun clear() {
_items.value = emptyList()
}
}

// 在任何 Composable 中使用
@Composable
fun CartBadge() {
val count by CartManager.totalCount.collectAsState()
Badge { Text("$count") }
}

⚠️ 全局单例要慎用:测试难隔离、生命周期不可控。优先考虑 ViewModel + 依赖注入(Hilt/Koin)。

性能优化

状态粒度 —— 越细越好

1
2
3
4
5
6
7
8
9
// ❌ 一个大状态 → 任何字段变化都触发所有 UI 重组
data class BigState(val a: Int, val b: Int, val c: Int, val d: Int)

Row {
Text("a: ${state.a}") // a 变了,全部重组
Text("b: ${state.b}")
Text("c: ${state.c}")
Text("d: ${state.d}")
}
1
2
3
4
5
6
7
8
9
10
11
12
// ✅ 细粒度状态 → 只重组需要的地方
val a by viewModel.flowA.collectAsState()
val b by viewModel.flowB.collectAsState()
val c by viewModel.flowC.collectAsState()
val d by viewModel.flowD.collectAsState()

Row {
Text("a: $a") // 只有 a 变化时这里才重组
Text("b: $b") // 只有 b 变化时这里才重组
Text("c: $c")
Text("d: $d")
}

🔑 权衡:单一 StateFlow 方便管理,但可能导致不必要的重组。如果页面复杂且有独立的 UI 区域,拆分为多个 StateFlow。

derivedStateOf —— 缓存计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun ItemList(viewModel: ItemViewModel = viewModel()) {
val items by viewModel.items.collectAsState()
val filter by viewModel.filter.collectAsState()

// ✅ derivedStateOf:仅 items 或 filter 变化时重新计算
val filteredItems by remember {
derivedStateOf {
items.filter { it.matches(filter) }
}
}

LazyColumn {
items(filteredItems, key = { it.id }) { item ->
ItemRow(item)
}
}
}

derivedStateOf 内部会做相等性比较,只有结果变化才通知重组。

常见坑与最佳实践

原因解决
旋转屏幕状态丢失用了 remember 没用 ViewModel页面级状态用 ViewModel
ViewModel 中创建 State 导致内存泄漏在 ViewModel init 里持有 Composable 引用ViewModel 只管数据,不管 UI
collectAsState() 阻塞主线程默认在主线程 collectFlow 内部用 flowOn(Dispatcher.IO)
多个 ViewModel 间状态不同步各自维护同一份数据提取到 Repository 或共享 StateFlow
CompositionLocal 导致全树重组使用了 compositionLocalOf 且值频繁变化staticCompositionLocalOf
StateFlow 热流导致后台持续计算WhileSubscribed(5000) 默认永不停设置合适的 stopTimeoutMillis
mutableStateOf 放错位置放在 ViewModel 但不用 collectAsStateViewModel 中用 StateFlow,Composable 中用 mutableStateOf

架构速查表

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
┌─────────────────────────────────────────────────────┐
│ Activity / Fragment │
│ ┌───────────────────────────────────────────────┐ │
│ │ setContent { AppTheme { NavHost { ... } } } │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Screen A │ │ Screen B │ │ Screen C │ │
│ │ │ │ │ │ │ │
│ │viewModel │ │ viewModel │ │ viewModel │ │
│ │ .state │ │ .state │ │ .state │ │
│ │ ↓ │ │ ↓ │ │ ↓ │ │
│ │collectA- │ │ collectA- │ │ collectA- │ │
│ │sState() │ │ sState() │ │ sState() │ │
│ │ ↓ │ │ ↓ │ │ ↓ │ │
│ │ UI ← │ │ UI ← │ │ UI ← │ │
│ │ Event │ │ Event │ │ Event │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Repository Layer (StateFlow) │ │
│ │ ┌────────────┐ ┌──────────────────────┐ │ │
│ │ │CartManager │ │ UserSessionManager │ │ │
│ │ │(Singleton) │ │ (Singleton) │ │ │
│ │ └────────────┘ └──────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Data Layer (Room / DataStore / Retrofit) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘

完整实战:搜索 + 收藏功能

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// ====== UiState ======
data class SearchUiState(
val query: String = "",
val results: List<Repo> = emptyList(),
val favorites: Set<String> = emptySet(),
val isLoading: Boolean = false,
val error: String? = null
)

// ====== Event ======
sealed interface SearchEvent {
data class QueryChanged(val query: String) : SearchEvent
data object Search : SearchEvent
data class ToggleFavorite(val repoId: String) : SearchEvent
}

// ====== ViewModel ======
class SearchViewModel(
private val repository: RepoRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {

private val _state = MutableStateFlow(SearchUiState())
val state: StateFlow<SearchUiState> = _state.asStateFlow()

init {
// 恢复进程杀死前的查询
savedStateHandle.get<String>("query")?.let { query ->
_state.update { it.copy(query = query) }
}
}

fun onEvent(event: SearchEvent) {
when (event) {
is SearchEvent.QueryChanged -> {
_state.update { it.copy(query = event.query) }
savedStateHandle["query"] = event.query
}

SearchEvent.Search -> {
val query = _state.value.query.ifBlank { return }
_state.update { it.copy(isLoading = true, error = null) }
viewModelScope.launch {
try {
val results = repository.search(query)
_state.update { it.copy(isLoading = false, results = results) }
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.message) }
}
}
}

is SearchEvent.ToggleFavorite -> {
_state.update { state ->
val newFavorites = state.favorites.toMutableSet().apply {
if (contains(event.repoId)) remove(event.repoId)
else add(event.repoId)
}
state.copy(favorites = newFavorites)
}
}
}
}
}

// ====== UI ======
@Composable
fun SearchScreen(
viewModel: SearchViewModel = viewModel(),
onRepoClick: (String) -> Unit
) {
val state by viewModel.state.collectAsState()

Scaffold(
topBar = {
SearchBar(
query = state.query,
onQueryChange = { viewModel.onEvent(SearchEvent.QueryChanged(it)) },
onSearch = { viewModel.onEvent(SearchEvent.Search) }
)
}
) { padding ->
when {
state.isLoading -> Box(
Modifier.fillMaxSize().padding(padding),
contentAlignment = Alignment.Center
) { CircularProgressIndicator() }

state.error != null -> ErrorRetry(state.error!!) {
viewModel.onEvent(SearchEvent.Search)
}

state.results.isEmpty() && state.query.isNotEmpty() -> EmptyResult()

else -> LazyColumn(modifier = Modifier.padding(padding)) {
items(state.results, key = { it.id }) { repo ->
RepoItem(
repo = repo,
isFavorite = repo.id in state.favorites,
onFavorite = { viewModel.onEvent(SearchEvent.ToggleFavorite(repo.id)) },
onClick = { onRepoClick(repo.id) }
)
}
}
}
}
}

以上模式可以无痛扩展到任意页面:定义 UiState → 定义 Event → ViewModel 处理 Event 产生新 State → UI 收集。