Jetpack Compose 状态提升(State Hoisting)完全指南

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")
            }
        }
    }
}

问题在哪?

  1. expanded 是普通变量,不是 State
  2. 即使改成 mutableStateOf,状态也在子组件内部
  3. 父组件无法控制它展开还是折叠
  4. 无法在多个地方同步这个状态

✅ 解决方案:状态提升

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

相关推荐
数智工坊33 分钟前
机器人运动控制:采样、优化与学习三大流派深度对比与实战
android·学习·机器人
故渊at2 小时前
第二板块:Android 四大组件标准化学理 | 第八篇:Service 后台执行实体与优先级
android·gitee·service·前台服务·后台服务
会Tk矩阵群控的小木3 小时前
安卓群控系统对于游戏工作室实战教程
android·运维·游戏·adb·开源软件·个人开发
qeen873 小时前
【C++】类与对象之类的默认成员函数(二)
android·c语言·开发语言·c++·笔记·学习
故渊at4 小时前
第二板块:Android 四大组件标准化学理 | 第九篇:BroadcastReceiver 事件分发与有序广播
android·gitee·broadcast·广播·动态注册·静态注册
JohnnyDeng944 小时前
【Android】Room 数据库高级用法与性能调优:从查询瓶颈到毫秒级响应
android·性能优化·kotlin·room
zeqinjie4 小时前
Flutter 折叠屏 iPad / 宽屏适配实践
android·前端·flutter
ab_dg_dp4 小时前
Android 17+ 提取 AIDL 生成 Java 文件的实用脚本
android·java·python
Arrom5 小时前
DLNA 渲染端排障实战:从 20s 卡顿到 stale subscriber 的两周追凶之旅
android·java