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常用值变化带动画扩展·动画
选型口诀:单值 → StateOf要抗旋转 → Saveable 或 VM列表增删 → StateList / VM 列表算出来的 → derived换对象重置 → key / remember(keys)网络/业务 → VM + StateFlow

状态两种持有

概念

状态放哪儿,先问:离开界面还要不要? 多个界面要不要共享?

和 XML 对照

持有XML 直觉Compose
界面局部字段 + setXxxremember / Saveable / StateList
业务状态ViewModel + LiveData/FlowViewModel + 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/DoublemutableIntStateOf 等,少装箱(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 默认只吃基本类型、StringParcelable 等。自定义 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
旧模块 LiveDataobserveAsState(过渡)
无 VM 的小组件异步produceState
长 Effect 要最新回调rememberUpdatedState
主题/全局间距CompositionLocal / MaterialTheme
数值变化要过渡动画animate*AsState(动画章)
▶ 登录页:局部 → 提升 → ViewModel

先局部跑通表单 → 提升方便 Preview → 校验/请求进 VM,界面只吃 StateFlow。

下一章:副作用边界。SharedFlow 事件如何在界面消费,也在那里衔接。
Android Compose 现代开发知识体系 · 主线必读 / 扩展选读。
术语表常驻;组件与属性先看分讲,再查两张总表。案例默认收起。