可观察状态
状态类型与核心特点对照表
| 状态类型 | 核心特点 | 典型场景 |
|---|---|---|
mutableStateOf |
基础单值可观察状态,修改 value 触发重组 |
计数器、开关、输入框文本、按钮状态 |
mutableStateListOf |
列表元素变化触发重组 | 购物车、待办清单、动态列表 |
mutableStateMapOf |
Map 键值对变化触发重组 | 用户配置、筛选条件、键值对数据 |
StateFlow + collectAsState |
跨组件共享状态,支持异步 | 页面级共享状态、异步数据更新 |
derivedStateOf |
派生计算,仅结果变化触发重组(性能优化) | 滚动阈值、多状态组合计算 |
Flow + collectAsState |
普通 Flow 转可观察状态 | 单次异步请求(网络/数据库) |
remember ()
remember() 是 Compose 的 "状态存储器",作用是在组件重组时保留状态值,避免每次重组都重新创建状态(比如mutableStateOf)。
通俗理解
把 Compose 重组比作 "教室换座位":
没有remember():每次换座位,你都把笔记本扔了,重新拿个新本(mutableStateOf(0)重新创建,值重置为 0);
有remember():换座位时把笔记本装在书包里带走(状态被保留,值不会重置);
注意:remember() 只在 "同一重组作用域" 内有效 ------ 如果组件被销毁重建(比如页面跳转后返回),状态还是会丢失(此时需要用rememberSaveable,但核心是remember的 "记忆" 特性)。
踩坑提醒:一定要加 remember!
如果忘记加remember(),会出现 "点击加 1 后,一重组就变回 0" 的诡异问题:
java
@Composable
fun RememberPitfallDemo() {
// 错误:无remember,每次重组都重置为0
val noRememberState = mutableStateOf(0)
// 正确:有remember,重组时保留值
val rememberState = remember { mutableStateOf(0) }
Column {
Text("无remember:${noRememberState.value}") // 永远是0(重组就重置)
Text("有remember:${rememberState.value}") // 保留当前值
Button(onClick = {
noRememberState.value++
rememberState.value++
}) { Text("点击加1") }
}
}
日常开发中,remember和mutableStateOf几乎是 "绑定使用" 的:
java
// 标准写法:创建可观察状态并保留
var count by remember { mutableStateOf(0) }
= vs by
| 写法 | 变量类型 | 读取方式 | 修改方式 | 本质 | 适用场景 |
|---|---|---|---|---|---|
val state = remember { mutableStateOf(0) } |
MutableState<Int>(容器) |
state.value |
state.value++ |
直接持有状态容器本身 | 需要操作容器(传递/比较) |
var count by remember { mutableStateOf(0) } |
Int(容器内的值) |
count |
count++ |
通过委托访问容器内的值 | 简化语法,直接操作值 |
通俗理解
把MutableState比作 "带盖子的盒子":
= 写法:你直接拿到了 "整个盒子",要拿 / 放里面的东西,必须先打开盖子(.value);
by 写法:你委托别人帮你管盒子,不用碰盒子本身,直接拿 / 放里面的东西(编译器自动开盖子)。
by 是 Kotlin 的委托语法糖,Compose 给MutableState实现了ReadWriteProperty接口,编译器会自动帮你补全.value:
java
// by 写法的等价代码(编译器自动生成)
val countDelegate = remember { mutableStateOf(0) }
var count: Int
get() = countDelegate.value // 读取时自动加.value
set(value) { countDelegate.value = value } // 修改时自动加.value
注意点
by 写法必须用var(因为要修改值),用val会编译报错;
= 写法建议用val(容器本身不用重新赋值,只改内部.value);
两种写法的重组效果完全一致,没有性能差异,仅语法不同。
无状态 + 状态提升 + 单向数据流
| 概念 | 核心定义 |
|---|---|
| 无状态组件 | 组件内部不持有状态,所有状态由外部传入,仅负责"展示 UI"和"转发事件" |
| 状态提升 | 将组件的内部状态"提到"父组件管理,子组件通过参数接收状态和回调函数 |
| 单向数据流 | 状态只能"从父到子"传递(读),修改需通过子组件调用父组件回调(写),形成闭环 |
通俗理解:用 "餐厅点餐" 类比
无状态组件 = 服务员:不记顾客点了什么(不持有状态),只负责把点餐需求传给前台(父组件),把菜品端给顾客(展示 UI);
状态提升 = 点餐状态交给前台:服务员(子组件)不存菜单,所有点餐信息集中在前台(父组件),避免多个服务员记的菜单不一致;
单向数据流 = 点餐流程:顾客→服务员→前台→后厨(状态传递:父→子),后厨做好菜→前台→服务员→顾客(事件回调:子→父),全程单向,不混乱。
反例 vs 正确示例
反例(有状态组件,不推荐)
子组件自己持有状态,父组件无法控制,复用性差:
java
// 有状态子组件:内部持有count,父组件无法获取/修改
@Composable
fun BadCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("计数:$count") }
}
正确示例(无状态 + 状态提升 + 单向数据流)
java
// 1. 无状态子组件:只接收状态和回调,不持有状态
@Composable
fun GoodCounter(
count: Int, // 从父组件接收状态(读)
onCountAdd: () -> Unit // 从父组件接收回调(写)
) {
Button(onClick = onCountAdd) { Text("计数:$count") }
}
// 2. 父组件:管理状态,实现单向数据流
@Composable
fun CounterParent() {
var count by remember { mutableStateOf(0) } // 父组件持有状态
Column {
// 状态从父到子(读),修改通过回调(写)→ 单向数据流
GoodCounter(count = count, onCountAdd = { count++ })
Text("父组件同步显示:$count") // 父组件可复用状态,灵活扩展
}
}
核心优势
无状态组件:可复用性强(同一个组件传入不同状态就能展示不同内容)、易测试(传固定状态即可验证 UI);
状态提升:状态集中管理,避免多个组件持有同一份状态导致数据不一致;
单向数据流:状态变化可追踪,所有修改都通过回调函数,调试时能快速找到 "谁改了状态"。