Jetpack Compose 状态提升(State Hoisting)完全指南
基于官方 Codelab 整理,专为 Android 开发者打造
一、什么是状态提升?
一句话定义:状态提升是一种设计模式,将子组件的状态移动到父组件中,让父组件成为"单一事实来源"。
核心思想
❌ 错误做法:状态藏在子组件里
子组件自己记状态 → 父组件不知道 → 无法控制
✅ 正确做法:状态提升到父组件
父组件记状态 → 传给子组件 → 子组件只负责显示
二、从问题出发:为什么需要状态提升?
场景:点击按钮展开/折叠内容
❌ 问题代码:状态在子组件内部
kotlin
@Composable
fun Greeting(name: String) {
// 问题:状态在子组件内部
var expanded = false // 普通变量,改了也不会重组!
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
// 根据 expanded 显示/隐藏内容
if (expanded) {
Text(text = "这是隐藏的内容")
}
}
ElevatedButton(onClick = {
expanded = !expanded // 改了也不会触发重组!
}) {
Text("Show more")
}
}
}
}
问题在哪?
expanded是普通变量,不是State- 即使改成
mutableStateOf,状态也在子组件内部 - 父组件无法控制它展开还是折叠
- 无法在多个地方同步这个状态
✅ 解决方案:状态提升
kotlin
// 步骤 1:子组件接收状态 + 事件回调
@Composable
fun Greeting(
name: String,
expanded: Boolean, // 状态从父组件传来
onExpandedChange: (Boolean) -> Unit // 事件回调给父组件
) {
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
if (expanded) {
Text(text = "这是隐藏的内容")
}
}
ElevatedButton(onClick = {
onExpandedChange(!expanded) // 通知父组件改状态
}) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
kotlin
// 步骤 2:父组件持有状态
@Composable
fun MyApp() {
// 状态在这里!父组件是"单一事实来源"
var expanded by remember { mutableStateOf(false) }
Greeting(
name = "Android",
expanded = expanded, // 向下传递状态
onExpandedChange = { expanded = it } // 接收事件更新状态
)
}
三、状态提升的完整流程图
┌─────────────────────────────────────────────────────────┐
│ 父组件 (MyApp) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ var expanded by remember { mutableStateOf(false) } │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ 状态向下传递 │ 事件向上传递 │
│ expanded ───────┼──────> onExpandedChange │
│ │ │
└─────────────────────────┼────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 子组件 (Greeting) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ fun Greeting( │ │
│ │ expanded: Boolean, │ │
│ │ onExpandedChange: (Boolean) -> Unit │ │
│ │ ) │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ 只读状态 │ 只负责通知事件 │
│ │ │
└─────────────────────────────────────────────────────────┘
四、状态提升的三大好处
1️⃣ 可复用(Reusable)
子组件不依赖固定状态,拿到什么显示什么:
kotlin
// 同一个 Greeting,可以用在不同地方
@Composable
fun Screen1() {
var expanded by remember { mutableStateOf(true) } // 默认展开
Greeting("A", expanded, { expanded = it })
}
@Composable
fun Screen2() {
var expanded by remember { mutableStateOf(false) } // 默认折叠
Greeting("B", expanded, { expanded = it })
}
2️⃣ 可测试(Testable)
可以单独测试子组件,传不同的状态值:
kotlin
@Preview
@Composable
fun GreetingExpandedPreview() {
Greeting("Test", expanded = true, onExpandedChange = {})
}
@Preview
@Composable
fun GreetingCollapsedPreview() {
Greeting("Test", expanded = false, onExpandedChange = {})
}
3️⃣ 可控制(Controllable)
父组件掌握数据,逻辑清晰,多个组件可以共享状态:
kotlin
@Composable
fun ParentScreen() {
var expanded by remember { mutableStateOf(false) }
Column {
// 两个 Greeting 共享同一个状态
Greeting("A", expanded, { expanded = it })
Greeting("B", expanded, { expanded = it })
// 父组件可以强制控制
Button(onClick = { expanded = true }) {
Text("全部展开")
}
}
}
五、状态提升口诀
状态跟谁强相关,就放在谁最近的地方。
- 单个组件用 → 放在组件内部
- 多个组件共享 → 提升到最近的共同父组件
- 涉及业务逻辑 → 提升到 ViewModel
决策树
状态需要被谁读取/修改?
│
├── 只有这一个组件 → 保持在内部
│ └── 用 remember { mutableStateOf() }
│
├── 多个组件需要 → 提升到共同父组件
│ └── 父组件用 remember { mutableStateOf() }
│
└── 涉及业务逻辑/跨屏幕 → 提升到 ViewModel
└── ViewModel 用 mutableStateOf / StateFlow
六、实战:完整展开列表示例
最终代码
kotlin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
}
}
}
@Composable
fun MyApp(modifier: Modifier = Modifier) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background
) {
Column {
Greeting(name = "Android")
Greeting(name = "Compose")
Greeting(name = "State")
}
}
}
@Composable
fun Greeting(
name: String,
expanded: Boolean = false,
onExpandedChange: (Boolean) -> Unit = {}
) {
Surface(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
if (expanded) {
Text(
text = "这是 $name 的隐藏内容",
modifier = Modifier.padding(top = 8.dp)
)
}
}
ElevatedButton(onClick = {
onExpandedChange(!expanded)
}) {
Text(if (expanded) "Show less" else "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
BasicsCodelabTheme {
MyApp()
}
}
七、常见问题 FAQ
Q1:每个 Greeting 都要独立控制展开/折叠怎么办?
kotlin
@Composable
fun MyApp() {
// 用 Map 存储每个 name 的展开状态
var expandedMap by remember { mutableStateOf(mapOf<String, Boolean>()) }
Column {
listOf("Android", "Compose", "State").forEach { name ->
Greeting(
name = name,
expanded = expandedMap[name] ?: false,
onExpandedChange = {
expandedMap = expandedMap + (name to it)
}
)
}
}
}
Q2:状态提升后代码好啰嗦,有没有简化写法?
有!用 Kotlin 的 by 委托:
kotlin
// 啰嗦写法
var count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) { Text("${count.value}") }
// 简洁写法
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
Q3:什么时候不应该状态提升?
当状态只在一个组件内部使用,且不需要外部控制时:
kotlin
@Composable
fun ChatBubble(message: Message) {
// 这个状态只在内部用,不需要提升
var showDetails by rememberSaveable { mutableStateOf(false) }
ClickableText(
text = AnnotatedString(message.content),
onClick = { showDetails = !showDetails }
)
if (showDetails) {
Text(message.timestamp)
}
}
八、总结
| 概念 | 说明 |
|---|---|
| 状态提升 | 把状态从子组件移到父组件 |
| 单一事实来源 | 状态只有一个地方管理,避免多份副本 |
| 向下传递 | 父组件把状态传给子组件 |
| 向上传递 | 子组件通过回调通知父组件改状态 |
核心模式
kotlin
// 父组件:持有状态
var state by remember { mutableStateOf(initialValue) }
// 子组件:接收状态 + 事件回调
ChildComposable(
state = state,
onStateChange = { state = it }
)
参考资料
最后更新:2026-05-19
整理自 Android 官方 Codelab