E状态与数据流
主线最易踩坑的一章。按真实开发频率补齐各类 State:局部值 / 集合 / 派生 / key 重置 / Saveable·Saver、Flow 桥接(collect / snapshotFlow)、VM UiState(含 combine·stateIn),以及偶尔用到的 LiveData / produceState / Updated / Local。术语见术语表。
State 全家桶(按频率)
| 类型 | 频率 | 一句话 | 典型场景 |
|---|---|---|---|
mutableStateOf + remember | 常用 | 单个可观察值 | 开关、计数、弹窗显隐 |
rememberSaveable | 常用 | 抗旋转的简单值 | 输入框草稿、Tab 下标 |
mutableIntStateOf 等 | 常用 | 基本类型少装箱 | 高频 Int/Float 计数、进度 |
mutableStateListOf / Map | 常用 | 可观察集合 | 本地待办增删、选中集合 |
derivedStateOf | 常用 | 由其他 State 算出 | 表单校验、列表过滤结果 |
remember(keys) / key { } | 常用 | 换身份时重置状态 | 切换 userId、列表项复用 |
StateFlow + collectAsStateWithLifecycle | 常用 | VM 快照 → UI State | 几乎所有业务页 |
VM combine + stateIn | 常用 | 多源合成一份 UiState | 筛选+分页+加载态同屏 |
密封 UiState | 常用 | 加载/成功/失败 | 列表、详情、提交 |
SharedFlow 一次性事件 | 常用 | 导航 / Snackbar | 登录成功跳转(配合 Effect) |
自定义 Saver | 偶尔 | Saveable 复杂对象 | 自定义 data class 草稿 |
snapshotFlow | 偶尔 | State → Flow | 滚动位置防抖、桥到仓库 |
LiveData.observeAsState | 偶尔 | 旧栈桥接 | 遗留 VM 仍用 LiveData |
produceState | 偶尔 | 组合内把异步收成 State | 轻量读本地、不配 VM 的岛 |
rememberUpdatedState | 偶尔 | Effect 里总用最新回调 | LaunchedEffect 长任务 |
CompositionLocal | 偶尔 | 隐式向下传依赖 | 主题、间距、分析器 |
animate*AsState | 常用 | 值变化带动画 | 见扩展·动画 |
状态两种持有
概念
状态放哪儿,先问:离开界面还要不要? 多个界面要不要共享?
和 XML 对照
| 持有 | XML 直觉 | Compose |
|---|---|---|
| 界面局部 | 字段 + setXxx | remember / Saveable / StateList |
| 业务状态 | ViewModel + LiveData/Flow | ViewModel + StateFlow → collect |
易错点
网络与复杂校验塞进 Composable → 难测,也和重组搅在一起。
remember + mutableStateOf 常用
概念
State:可读可写、变化通知重组。mutableStateOf 造它;remember 让它活过多次重组。
怎么用
@Composable
fun CounterBasics() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("$count")
}
}
等价显式写法:val count = remember { mutableStateOf(0) },读写 count.value。推荐 by。
基本类型优化 常用
高频 Int/Long/Float/Double 用 mutableIntStateOf 等,少装箱(API 34+ / 较新 Runtime):
var progress by remember { mutableFloatStateOf(0f) }
var page by rememberSaveable { mutableIntStateOf(0) }
remember:每次重组重置。普通 var x = 0:改了不刷新。▶ 计数器 / 开关
@Composable
fun CounterOrSwitch() {
var count by remember { mutableIntStateOf(0) }
var on by remember { mutableStateOf(false) }
Column {
Button(onClick = { count++ }) { Text("计数 $count") }
Switch(checked = on, onCheckedChange = { on = it })
}
}
rememberSaveable 常用
概念
remember 只抗重组;旋转 / 进程恢复会丢。rememberSaveable ≈ Bundle。
怎么用
var name by rememberSaveable { mutableStateOf("") }
var tab by rememberSaveable { mutableIntStateOf(0) }
易错点
- 不可自动序列化的对象 → 自定义
Saver(下节),或放 ViewModel。 - 大列表 / 用户会话只靠 Saveable → 不够。
▶ 旋转后输入还在
自定义 Saver 偶尔
概念
rememberSaveable 默认只吃基本类型、String、Parcelable 等。自定义 data class 要提供 Saver,或改放 ViewModel。
怎么用
data class Draft(val title: String, val body: String)
val DraftSaver = listSaver<Draft, Any>(
save = { listOf(it.title, it.body) },
restore = { Draft(it[0] as String, it[1] as String) }
)
@Composable
fun Editor() {
var draft by rememberSaveable(stateSaver = DraftSaver) {
mutableStateOf(Draft("", ""))
}
// …
}
易错点
能塞 Bundle 的才 Saveable;会话级大对象、仓库缓存 → ViewModel / 磁盘,别硬塞 Saver。
mutableStateListOf / mutableStateMapOf 常用
概念
普通 List/Map 改内容 Compose 感知不到。可观察集合:增删改会触发读过它的地方重组。
和 XML 对照
≈ 改 Adapter 数据后 notify…;这里改集合本身即可。
怎么用
@Composable
fun TodoLocal() {
val todos = remember { mutableStateListOf("买菜", "写代码") }
val checked = remember { mutableStateMapOf<String, Boolean>() }
Column {
Button(onClick = { todos.add("跑步") }) { Text("添加") }
todos.forEach { item ->
val on = checked[item] == true
Row {
Checkbox(
checked = on,
onCheckedChange = { checked[item] = it }
)
Text(item)
}
}
}
}
易错点
list = list + x换引用可以,但原地list.add对普通 List 无效。- 业务大列表更常放 VM 的
StateFlow<List>,局部 StateList 适合轻量 UI 草稿。
▶ 待办增删自动刷新
derivedStateOf 常用
概念
值由其他 State 计算得出。用 derivedStateOf 可在依赖不变时跳过无用下游重组(列表过滤、表单是否可提交等)。
怎么用
@Composable
fun FilterDemo(all: List<String>) {
var query by rememberSaveable { mutableStateOf("") }
// query 是 State → derived 会订阅;all 是普通参数 → 放进 remember key
val filtered by remember(all) {
derivedStateOf {
all.filter { it.contains(query, ignoreCase = true) }
}
}
// …
}
@Composable
fun FormReady(user: String, pwd: String) {
// 参数本身不是 State:直接算即可(或把依赖放进 remember 的 key)
val canSubmit = user.isNotBlank() && pwd.length >= 6
Button(onClick = { }, enabled = canSubmit) { Text("提交") }
}
和「直接算」的差别
简单表达式可直接 val can = …(每次重组重算,通常也够)。依赖复杂、下游很重(大列表过滤)时再用 derived。
易错点
在 derived 里读普通参数/外部 var:它们不是 State,不会触发派生重算。要么改成 State,要么放进 remember(…) 的 key。
▶ 搜索关键字过滤列表
remember(keys) / key { } 常用
概念
状态跟「身份」绑定:换用户、换详情 id,旧 State 必须丢掉。否则会出现「上一个用户的草稿还在」。
怎么用
@Composable
fun Profile(userId: String) {
// keys 变了 → 重新执行 lambda,拿到新 State
var draft by remember(userId) { mutableStateOf("") }
TextField(value = draft, onValueChange = { draft = it })
}
@Composable
fun Feed(items: List<Item>) {
items.forEach { item ->
// 子树整体换 key:内部 remember 全部重置
key(item.id) {
ItemRow(item)
}
}
}
和 XML 对照
≈ 换业务 id 时清字段 / 换 Adapter item 时别错绑 ViewHolder 状态。
易错点
- 详情页用固定
remember { }不带 id → 返回再进别的 id 仍显示旧数据。 LazyColumn更常用items(…, key = { it.id }),与这里的key { }同思路。
▶ 切换 userId 清空草稿
状态提升 常用
概念
状态提升:父持有;子只要「当前值 + 回调」。
@Composable
fun SwitchRow(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
label: String
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(label, modifier = Modifier.weight(1f))
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}
▶ 子开关不自己藏状态
父级 var notify by rememberSaveable { mutableStateOf(true) },把 checked/onCheckedChange 传入。便于 Preview 与复用。
单向数据流 + UiState 常用
概念
单向数据流:State 向下,事件向上。业务页用密封 UiState 表达互斥界面。
sealed interface UiState<out T> {
data object Loading : UiState<Nothing>
data class Success<T>(val data: T) : UiState<T>
data class Error(val msg: String) : UiState<Nothing>
}
▶ 加载 / 成功 / 失败三态界面
when (state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> /* 列表 */
is UiState.Error -> Text(state.msg)
}
ViewModel + StateFlow 常用
概念
业务真相放 ViewModel。对外暴露 StateFlow<Ui>(当前快照);一次性事件用 SharedFlow/Channel(Snackbar、导航)。
class LoginViewModel : ViewModel() {
private val _ui = MutableStateFlow(LoginUi())
val ui: StateFlow<LoginUi> = _ui.asStateFlow()
private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()
fun onUserChange(v: String) = _ui.update { it.copy(user = v) }
fun login() { /* 成功后 viewModelScope.launch { _events.emit("ok") } */ }
}
data class LoginUi(val user: String = "", val pwd: String = "") {
val canLogin get() = user.isNotBlank() && pwd.length >= 6
}
多源合成:combine + stateIn 常用
真实业务常有「查询词 + 仓库 Flow + 加载标志」多路输入。在 VM 里 combine 成一份 UiState,再用 stateIn 变成热流给界面收:
val ui: StateFlow<ListUi> = combine(query, items, loading) { q, list, load ->
ListUi(q, list.filter { it.contains(q) }, load)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ListUi()
)
易错点
ViewModel()自己 new → 用viewModel()。- 用 StateFlow 发「只消费一次」的导航 → 旋转可能再吃;事件用 SharedFlow。
SharingStarted.Eagerly无订阅也跑;界面页优先WhileSubscribed。
▶ 登录:校验在 VM,界面只渲染
@Composable
fun LoginRoute(vm: LoginViewModel = viewModel()) {
val ui by vm.ui.collectAsStateWithLifecycle()
LoginScreen(ui, vm::onUserChange, vm::onPwdChange, vm::login)
}
collectAsStateWithLifecycle 常用
概念
把 Flow/StateFlow 按生命周期收成 Compose State。依赖 lifecycle-runtime-compose。
val ui by vm.ui.collectAsStateWithLifecycle()
和 collectAsState
collectAsState() 不感知 Lifecycle,后台也可能继续收集。新代码优先 WithLifecycle。
▶ 页面收集 StateFlow
snapshotFlow 偶尔
概念
把「读 Compose State」变成 Flow:State 一变就 emit。常用于滚动位置、把 UI 状态桥回仓库 / 做防抖(配合 Effect)。
@Composable
fun SaveScroll(listState: LazyListState) {
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.debounce(300)
.collect { index -> repo.saveScroll(index) }
}
}
易错点
snapshotFlow 块里只读 State / 派生出的值;别在里面发网络。收集放在 LaunchedEffect 等边界里。
▶ 列表滚动位置防抖落盘
XML 里常在 OnScrollListener 里记位置;这里用 State → Flow,语义更清晰。
LiveData.observeAsState 偶尔
概念
遗留 ViewModel 仍暴露 LiveData 时,桥成 Compose State。新代码优先 StateFlow。
val name by vm.nameLiveData.observeAsState(initial = "")
易错点
LiveData 与 StateFlow 混用两套 → 迁移期可以,长期统一到 Flow。
▶ 旧 VM 的 LiveData 接到界面
依赖 runtime-livedata。拿到 State<T> 后用法与 collect 相同。
produceState 偶尔
概念
在组合内把异步结果收成 State(内部是协程)。适合不配 VM 的轻量岛;正式业务页仍用 VM。
@Composable
fun ProfileName(userId: String) {
val name by produceState(initialValue = "…", userId) {
value = repo.fetchName(userId)
}
Text(name)
}
▶ 组合内读一次轻量数据
rememberUpdatedState 偶尔
概念
长生命周期的 LaunchedEffect(Unit) 里若直接捕获回调,可能拿到过期 lambda。用 rememberUpdatedState 总读最新。
@Composable
fun Tick(onTick: () -> Unit) {
val current by rememberUpdatedState(onTick)
LaunchedEffect(Unit) {
while (true) {
delay(1000)
current()
}
}
}
▶ Effect 里总调用最新回调
父级传入的 onTick 每次重组都可能是新函数;UpdatedState 避免 Effect 重启,又能调到新逻辑。
CompositionLocal 偶尔
概念
隐式向子树传依赖(主题、间距、Analytics)。滥用会让数据流变「隐形」;能参数传递就别 Local。
val LocalSpacing = compositionLocalOf { 8.dp }
@Composable
fun App(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalSpacing provides 12.dp) {
content()
}
}
@Composable
fun PaddedBox() {
val gap = LocalSpacing.current
Box(Modifier.padding(gap))
}
▶ 自定义间距 Local
MaterialTheme 本身就是一套 CompositionLocal。自建 Local 前先问:参数传递是否更清晰?
场景速查(对着选)
| 真实场景 | 优先选型 |
|---|---|
| 开关 / 弹窗显隐 / 简单计数 | remember { mutableStateOf } / IntState |
| 输入框、Tab 下标(怕旋转丢) | rememberSaveable |
| 本地待办增删、多选 id 集合 | mutableStateListOf / Map |
| 「能否提交」、搜索过滤结果 | 直接算或 derivedStateOf |
| 换 userId / 详情 id 要清空本地状态 | remember(id) 或 key(id) { } |
| 列表/详情/登录业务数据 | VM + StateFlow + collect |
| 多 Flow 合成一屏 | VM combine + stateIn |
| 登录成功跳转、Toast 一次 | VM SharedFlow + UI Effect |
| 复杂对象要扛旋转 | 自定义 Saver 或改放 VM |
| 滚动/滑块变化要进仓库 | snapshotFlow + Effect |
| 旧模块 LiveData | observeAsState(过渡) |
| 无 VM 的小组件异步 | produceState |
| 长 Effect 要最新回调 | rememberUpdatedState |
| 主题/全局间距 | CompositionLocal / MaterialTheme |
| 数值变化要过渡动画 | animate*AsState(动画章) |
▶ 登录页:局部 → 提升 → ViewModel
先局部跑通表单 → 提升方便 Preview → 校验/请求进 VM,界面只吃 StateFlow。