🕹 场景:App 如何"记住"自己的状态?
打开游戏 App,你会看到:
-
一个可滚动的英雄列表
-
一个聊天输入框
-
一个可以展开/收起的设置面板
你往上滑了几屏,在输入框打了一行字:"今天好累",然后点开设置面板......这时接到电话,App 被切到后台。
5 分钟后你回来------神奇的是:
-
列表还在刚才的位置
-
输入框里"今天好累"还在
-
设置面板还是展开的
👉 问题来了:App 是怎么"记住自己"的?
答案就是:每个 UI 元素都有自己的"小记忆"------在 Compose 里,这叫 界面元素状态(UI Element State)。
🤔 什么是"界面元素状态"?
界面元素状态 = 某个 UI 组件"自己内部的状态"
它不关心业务逻辑(比如用户是谁、游戏等级多少),只关心组件自身的状态:
-
"我滚到哪了?"
-
"我里面写了啥?"
-
"我是开着还是关着?"
常见例子:UI 元素与它的"小记忆"
| UI 元素 | 它的"小记忆"(界面元素状态) |
|---|---|
| LazyColumn | 当前滚动位置(第几项在顶部) |
| TextField | 用户输入的文字、光标位置 |
| Scaffold 抽屉 | 是否打开 |
| BottomSheet | 是展开、半开、还是收起 |
| Checkbox | 是否被勾选 |
这些状态天生属于组件自己,不需要 ViewModel 管理!
🔁 和"界面状态(UI State)"有什么区别?
很多人分不清这两个概念,核心差异可通过下表快速区分:
| 类型 | 谁关心? | 谁管理? | 例子 |
|---|---|---|---|
| 界面状态(UI State) | 整个页面/业务 | ✅ ViewModel | 用户信息、加载中状态、错误提示 |
| 界面元素状态(UI Element State) | 单个组件自己 | ✅ Composable 内部 | 滚动位置、输入框内容、抽屉开关 |
💡 一句话区分:
如果这个信息影响多个组件或业务逻辑 → 界面状态(交给 ViewModel)
如果只是某个组件自己的小习惯 → 界面元素状态(组件自己记)
🛠 怎么用?Compose 提供"专用记忆工具"
Compose 为常见组件提供了开箱即用的状态 API,无需从零造轮子,直接使用即可!
1️⃣ 滚动列表的记忆 → rememberLazyListState()
kotlin
@Composable
fun HeroList() {
// 这就是列表的"小记忆"
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(100) { index ->
Text("英雄 $index")
}
}
}
✅ 效果:滑到第 50 行 → 退出再回来 → 还在第 50 行!
2️⃣ 输入框的记忆 → mutableStateOf 或 TextFieldValue
kotlin
@Composable
fun ChatInput() {
// 输入框的"小记忆"
var text by remember { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = { text = it }, // 一改就更新记忆
label = { Text("说点什么...") }
)
}
✅ 效果:打字 → 切后台 → 回来文字还在!
💡 进阶:如果需要更精细控制(比如光标位置、选中文本),可用 TextFieldValue + remember { mutableStateOf(...) }:
kotlin
@Composable
fun AdvancedChatInput() {
// 包含光标位置、选中文本的完整记忆
var textState by remember {
mutableStateOf(TextFieldValue("初始文本", selection = TextRange(0, 4)))
}
OutlinedTextField(
value = textState,
onValueChange = { textState = it },
label = { Text("精细控制输入框") }
)
}
3️⃣ 抽屉/底部面板的记忆 → rememberDrawerState() / rememberModalBottomSheetState()
kotlin
@Composable
fun SettingsScreen() {
// 抽屉的"小记忆":默认关闭
val drawerState = rememberDrawerState(DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = { Text("设置选项:音效、画质、按键布局") }
) {
Scaffold(
topBar = {
TopAppBar( title = { Text("游戏主页") },
navigationIcon = {
IconButton(
onClick = {
// 打开抽屉(需协程作用域)
scope.launch { drawerState.open() }
}){
Icon(Icons.Default.Menu, contentDescription = "打开设置")
}
}
)
}
) { padding ->
// 主内容区(英雄列表、战斗入口等)
Box(modifier = Modifier.padding(padding)) {
Text("游戏主内容")
}
}
}
}
✅ 效果:抽屉打开 → 切后台 → 回来还是开着的(系统未杀进程情况下)!
4️⃣ 底部面板的记忆 → rememberModalBottomSheetState()
kotlin
@Composable
fun GameChatSheet() {
// 底部面板的"小记忆":默认隐藏
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
scope.launch { sheetState.hide() }
}){
// 聊天面板内容
ChatInput()
}
// 触发打开底部面板的按钮
Button(onClick = {
scope.launch { sheetState.show() }
}){
Text("打开聊天面板")
}
}
❓ 这些状态需要 ViewModel 吗?
绝大多数情况下:不需要!
原因:
-
它们是组件自身的细节,和业务无关;
-
Compose 已经帮你处理了重组时的状态保留;
-
放 ViewModel 反而会增加代码复杂度,违背"单一职责"。
✅ 例外情况(极少):
-
需要跨屏幕共享滚动位置(比如从英雄列表点进详情页,返回时回到原位置);
-
需要持久化保存(比如用户每次打开 App 都从上次滚动位置开始)。
这种情况下才考虑把状态提到 ViewModel,日常开发中基本用不到。
✅ 一句话总结
界面元素状态 = UI 组件自己的"小习惯"
Compose 已备好"记忆工具",直接用就行:
-
滚动?→ rememberLazyListState()
-
输入?→ remember { mutableStateOf("") }
-
抽屉?→ rememberDrawerState()
-
底部面板?→ rememberModalBottomSheetState()
你只管用,Compose 自动帮你记住组件的"小习惯"!