Compose 状态管理
深入 Jetpack Compose 状态管理体系:remember 与 rememberSaveable、mutableStateOf 与 by 委托、状态提升、derivedStateOf、CompositionLocal、SavedStateHandle 与单向数据流原则。
学习目标
- 掌握 remember 与 rememberSaveable 的区别与使用场景
- 理解 mutableStateOf 与 by 委托的状态读写机制
- 能够进行状态提升,编写无状态可复用组件
- 掌握 derivedStateOf 与 CompositionLocal 的使用时机
- 理解 SavedStateHandle 与配置变更/进程恢复的关系
学习目标
- 掌握
remember与rememberSaveable的区别与使用场景; - 理解
mutableStateOf与by委托的状态读写机制; - 能够进行状态提升,编写无状态(stateless)可复用组件;
- 掌握
derivedStateOf与CompositionLocal的使用时机; - 理解
SavedStateHandle与配置变更/进程恢复的关系,并贯彻单向数据流原则。
Compose 状态管理
Jetpack Compose 是声明式 UI 框架,其核心思想是 UI 是状态的函数:UI = f(state)。当状态变化时,Compose 会自动重新执行(重组,Recomposition)受影响的可组合函数,刷新对应界面。因此,“状态管理”在 Compose 中是比“命令式操作 View”更重要的基础课题。
依赖配置
dependencies {
// Compose BOM 统一管理版本
val composeBom = platform("androidx.compose:compose-bom:2024.09.03")
implementation(composeBom)
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.activity:activity-compose:1.9.2")
// 生命周期相关
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.6")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")
}
使用 BOM 后,所有
androidx.compose.*工件无需再写版本号,BOM 会保证彼此兼容。
状态的基本概念
在 Compose 中,状态(State) 是任何能驱动 UI 变化的值。最典型的形式是 MutableState<T>,它持有一个可变值,读取它的可组合函数会在值变化时自动重组。
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// 1. 直接读取 .value
val count = mutableStateOf(0)
// 读取:count.value
// 写入:count.value++
// 2. 使用 by 委托(推荐,代码更简洁)
var count by mutableStateOf(0)
// 读取:count
// 写入:count++
使用
by委托必须导入getValue与setValue两个扩展,否则编译器会报错。
remember:在重组间保留状态
可组合函数在状态变化时会被反复重新调用。如果直接在函数体内 val count = mutableStateOf(0),每次重组都会创建一个 新的 MutableState,状态永远归零。remember 的作用是把首次计算结果 存储在 Composition 中,后续重组直接复用:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun CounterScreen() {
// remember 把 mutableStateOf(0) 的结果缓存起来,重组时不会重新创建
var count by remember { mutableStateOf(0) }
Column {
Text("点击次数:$count")
Button(onClick = { count++ }) {
Text("加一")
}
}
}
remember 接受一个计算块,仅在该 Composable 首次进入组合(first composition)时执行,之后重组直接返回缓存的引用。
remember 的 key 依赖
remember(key1, key2) 在 key 变化时会 丢弃旧值并重新计算,常用于“参数变了状态要重置”的场景:
@Composable
fun UserHeader(userId: String) {
// userId 改变时,输入框内容会被重置
var draft by remember(userId) { mutableStateOf("") }
TextField(value = draft, onValueChange = { draft = it })
}
一个常见坑:把可变对象本身作为 key,但对象内部字段变了而引用没变,key 不会触发重新计算。务必使用值类型或稳定字段作为 key。
rememberSaveable:跨配置变更保留
remember 只能存活于“重组”,但旋转屏幕(配置变更)会销毁整个 Activity,remember 缓存随之丢失。rememberSaveable 会把值写入 SavedStateRegistry(最终走 onSaveInstanceState),配置变更后自动恢复:
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun LoginScreen() {
var username by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
// 旋转屏幕后,输入框内容仍在
TextField(value = username, onValueChange = { username = it })
}
rememberSaveable 默认支持基本类型(Int/String/Boolean 等以及它们的 MutableState)。对于自定义对象,必须让它能被保存:
- 方式一:实现
Parcelable接口; - 方式二:提供自定义
Saver。
Parcelable 方式
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserInfo(val id: String, val name: String, val avatar: String) : Parcelable
@Composable
fun ProfileScreen() {
var user by rememberSaveable { mutableStateOf(UserInfo("1", "Tom", "")) }
// ...
}
需要在
build.gradle中启用id("kotlin-parcelize")插件。
自定义 Saver
对于无法直接实现 Parcelable 的类型(如框架类、第三方类),可以提供 Saver:
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
data class SearchFilter(val keyword: String, val category: Int)
val SearchFilterSaver = Saver<SearchFilter, Pair<String, Int>>(
save = { it.keyword to it.category },
restore = { (keyword, category) -> SearchFilter(keyword, category) }
)
@Composable
fun SearchScreen() {
var filter by rememberSaveable(stateSaver = SearchFilterSaver) {
mutableStateOf(SearchFilter("", 0))
}
// ...
}
rememberSaveable与remember的关键差异:rememberSaveable走系统 SavedState,因此值必须能序列化;remember仅在内存中,可放任意对象(如CoroutineScope、Painter)。
状态提升:从有状态到无状态
状态提升(State Hoisting) 是把状态从组件内部移到调用方的模式。一个无状态(stateless)组件只接收“值”和“回调”,不自己持有状态。它的好处是:可复用、可测试、可预览。
对比下面两个版本:
// 有状态(stateful):内部维护状态,不易复用
@Composable
fun StatefulNameInput() {
var name by remember { mutableStateOf("") }
TextField(value = name, onValueChange = { name = it })
}
// 无状态(stateless):状态由调用方持有
@Composable
fun StatelessNameInput(
name: String,
onNameChange: (String) -> Unit
) {
TextField(value = name, onValueChange = onNameChange)
}
调用无状态组件时,调用方决定状态如何存储:
@Composable
fun FormScreen() {
var name by rememberSaveable { mutableStateOf("") }
StatelessNameInput(name = name, onNameChange = { name = it })
}
状态提升遵循三个原则:
- 状态提升到至少所有使用它的组件的共同父级;
- 状态只下传,事件只上传(单向数据流);
- 不可变状态向下,可变状态留在最上层。
经验法则:如果一个状态只被本组件使用,就放在组件里;被多个组件共享,就提升到共同父级。复杂场景下共享状态请放到 ViewModel(见下一章)。
derivedStateOf:派生状态
当一个状态是由其他状态计算得来时,用 derivedStateOf 可以避免重组过频。例如“列表是否滚动到顶部”由 scrollOffset 派生,但 scrollOffset 每帧都在变,没必要每次都重组依赖它的组件:
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
@Composable
fun TopBarShowcase(scrollProvider: () -> Int) {
// 只有当 showTopBar 真正翻转(false<->true)时才触发重组
val showTopBar by remember {
derivedStateOf { scrollProvider() < 100 }
}
AnimatedVisibility(visible = showTopBar) {
Text("回到顶部")
}
}
derivedStateOf 会缓存上次结果,只有当派生值 实际改变 时,订阅它的 Composable 才会重组。常用于:
- 按钮是否可点击(表单是否全部合法);
- 列表是否还有“加载更多”项;
- 输入计数
text.length > 0。
没必要给纯字符串模板(如
Text("count=$count"))加derivedStateOf,Compose 的快照系统已经足够智能。
CompositionLocal:隐式共享
CompositionLocal 提供“隐式向下传递”的方式,常用于主题色、字体、语言环境等“几乎所有组件都要用但参数透传太繁琐”的值。
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf
// 静态:值几乎不变,性能更好
val LocalThemeColor = staticCompositionLocalOf { Color.Black }
// 动态:值可能频繁变化
val LocalCurrentLocale = compositionLocalOf { "zh-CN" }
@Composable
fun AppRoot() {
CompositionLocalProvider(
LocalThemeColor provides Color.Blue,
LocalCurrentLocale provides "en-US"
) {
DeepChild()
}
}
@Composable
fun DeepChild() {
// 任意层级直接读取,无需层层透传参数
val color = LocalThemeColor.current
Box(Modifier.background(color))
}
慎用 CompositionLocal:它会让依赖关系隐式化,降低可测试性。仅用于真正的“全局环境”(主题、文字方向、ViewModelOwner)。业务数据应通过参数显式传递。
SavedStateHandle:ViewModel 级状态恢复
SavedStateHandle 是 ViewModel 用来持久化状态的组件,与 rememberSaveable 互补:
rememberSaveable持久化 Composable 内部的小型 UI 状态;SavedStateHandle持久化 ViewModel 持有的状态,进程被杀后也能恢复。
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
class EditorViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 直接读写 SavedStateHandle,进程恢复后值仍在
var content: String
get() = savedStateHandle.get<String>("content") ?: ""
set(value) { savedStateHandle["content"] = value }
fun save(text: String) {
content = text
}
}
Compose 中 ViewModel 通常通过 viewModel() 拿到(见下一章),其内部状态在配置变更和进程恢复时由 SavedStateHandle 兜底。
单向数据流(UDF)
Compose 的最佳实践是 单向数据流(Unidirectional Data Flow):
状态 (State) 事件 (Event)
ViewModel ─────────> UI ─────────> ViewModel
(单一可信源) (渲染) (用户操作)
- 状态从 ViewModel 流向 UI(通过
StateFlow/LiveData); - 事件从 UI 流向 ViewModel(调用 ViewModel 方法);
- ViewModel 持有唯一可信的状态,UI 只渲染不持有真相。
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() { _count.value++ }
fun decrement() { _count.value-- }
}
@Composable
fun CounterScreen(vm: CounterViewModel = viewModel()) {
val count by vm.count.collectAsStateWithLifecycle()
Column {
Text("count = $count")
Button(onClick = vm::increment) { Text("+") }
Button(onClick = vm::decrement) { Text("-") }
}
}
UDF 的好处:状态可追溯、可测试、不分散,UI 是纯函数。
实战:可分页的搜索框
下面把状态提升、derivedStateOf、rememberSaveable 串起来,做一个搜索输入框(保存输入、显示计数、清空按钮仅在非空时出现):
@Composable
fun SearchScreen() {
var keyword by rememberSaveable { mutableStateOf("") }
// 派生状态:只有 keyword 非空时才显示清空按钮
val showClear by remember(keyword) { derivedStateOf { keyword.isNotEmpty() } }
Column(Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
TextField(
value = keyword,
onValueChange = { keyword = it },
modifier = Modifier.weight(1f),
placeholder = { Text("输入关键字") }
)
if (showClear) {
IconButton(onClick = { keyword = "" }) {
Icon(Icons.Default.Clear, contentDescription = "清空")
}
}
}
Spacer(Modifier.height(8.dp))
Text("已输入 ${keyword.length} 个字符")
}
}
旋转屏幕后 keyword 保留;showClear 由 keyword 派生,不会冗余重组。
常见坑与最佳实践
- 忘记
remember,每次重组状态丢失:mutableStateOf必须包在remember/rememberSaveable里,否则每次重组都新建状态。 - 忘记导入
getValue/setValue:by委托需要这两个扩展函数,否则by mutableStateOf(0)报红。 - 状态没提升,组件无法复用:表单输入组件内部持有状态,调用方拿不到值。状态该提升就提升。
remember的 key 用了不稳定对象:List、data class(未加@Stable)等做 key 可能行为不符合预期。优先用String/Int等稳定类型。rememberSaveable放了无法序列化的对象:会运行时崩溃。要么实现Parcelable,要么提供Saver,要么改用remember。- 滥用
CompositionLocal传业务数据:会让组件测试困难、依赖隐式。CompositionLocal 只用于主题/环境类全局值。 - 在 Composable 里直接修改 ViewModel 字段:违背 UDF。应通过事件方法调用,状态只读。
derivedStateOf没包在remember里:每次重组都创建新的派生实例,失去缓存意义。务必remember { derivedStateOf { ... } }。- 在
remember代码块里调用其他 Composable:remember{}是普通 Kotlin 块,不能调用@Composable函数。 - 把
MutableState直接暴露给外部:对外只暴露不可变State<T>或值,自己保留可变入口。
章节小结
本章系统讲解了 Compose 状态管理:remember 在重组间缓存值,rememberSaveable 在配置变更间持久化(需要 Parcelable 或 Saver);mutableStateOf + by 委托是声明可变状态的标准写法;状态提升让组件变成可复用的纯函数;derivedStateOf 用于派生值的高效缓存;CompositionLocal 仅用于主题等全局环境值;SavedStateHandle 让 ViewModel 级状态在进程恢复后还原;最后单向数据流(状态下传、事件上传)是组织大型 Compose 应用的黄金法则。
下一章预告
下一章将把状态管理与架构层打通:在 Compose 中使用 viewModel() 获取 ViewModel、StateFlow.collectAsStateWithLifecycle 与 LiveData.observeAsState 把响应式数据接到 UI、用 LaunchedEffect/DisposableEffect/rememberCoroutineScope 管理副作用,以及 AndroidView/ComposeView 在新旧体系互操作场景的实战。