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

相关推荐
BoomHe5 小时前
git Rebase 为任意一笔提交补上 Change-Id
android·git·android studio
TDengine (老段)5 小时前
TDengine 超级表/子表/普通表 — 设计理念与内部表示
android·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
shuaiqinke6 小时前
【分享】Edge浏览器|内置扩展仓库|支持油猴|上网无限制
android·前端·人工智能·edge
Hali_Botebie6 小时前
岭回归(Ridge Regression),也称为L2正则化回归
数据挖掘·回归·kotlin
Carson带你学Android6 小时前
见证历史!Swift 6.3 官方支持 Android,跨平台要变天了?
android
plainGeekDev7 小时前
Android性能优化面试题:你说你会优化,结果连ANR都排查不了
android·面试
richard_yuu8 小时前
鸿蒙本地数据存储实战|Preferences 封装、数据隔离与隐私合规存储方案
android·华为·harmonyos
木易 士心8 小时前
深入理解 OKHttp:设计模式、核心机制与架构优势
android·设计模式·架构
Ehtan_Zheng8 小时前
Jetpack Compose `@ReadOnlyComposable` 的“魔法”
android