
var count by remember { mutableStateOf(0) }详解这一行代码是 Compose 中最经典的状态声明方式它其实包含了三个核心概念remember、mutableStateOf、Kotlin 委托by。下面逐一拆解。一、整体作用kotlinvar count by remember { mutableStateOf(0) }创建一个可观察的状态count初始值为0当count的值变化时Compose 会自动触发重组Recomposition重新执行组合函数remember保证在多次重组之间不丢失这个状态二、mutableStateOf可观察的状态容器1. 基本定义kotlinfun T mutableStateOf( initialValue: T, policy: SnapshotMutationPolicyT structuralEqualityPolicy() ): MutableStateT它创建一个MutableStateT对象kotlininterface MutableStateT : StateT { override var value: T }你可以把它理解为一个带监听器的盒子kotlinval count mutableStateOf(0) // 创建状态 println(count.value) // 读取0 count.value 5 // 写入2. 为什么是可观察的—— 快照系统SnapshotCompose 基于快照状态系统Snapshot State读取state.value时Compose 会把这个读取操作记录到当前的组合Composition中建立依赖写入state.value时系统会通知所有依赖它的读取方值变了于是 Compose 调度一次重组只重新执行受影响的组合函数类比mutableStateOf之于 Compose就像LiveData之于 View区别在于LiveData需要.observe()手动订阅而 Compose 的依赖收集是自动且细粒度的。3.value读写触发规则kotlinvar count by remember { mutableStateOf(0) }读Text(count: $count)→ 建立依赖写onClick { count }→ 触发重组by委托让我们直接写count而不是count.value三、remember跨重组缓存值1. 问题背景组合函数可能被频繁调用重组。如果每次都新建对象状态就丢了kotlinComposable fun Counter() { // ❌ 每次重组 count 都变回 0 var count mutableStateOf(0) }2.remember的作用kotlinval count remember { mutableStateOf(0) }第一次组合执行 lambda创建MutableState存入组合后续重组直接返回缓存的对象跳过 lambda组合被移除如导航离开、LazyColumn 滚动出屏幕缓存清除可以理解为 Compose 内部维护了一个键值缓存kotlin// 伪代码概念示意 slotTable[index] remember 的缓存槽3. 带 keys 的 remember当 key 变化时缓存作废、重新执行 lambdakotlin// 用户切换时头像重新加载 val avatar remember(userId) { loadAvatar(userId) }kotlin// 常用配置变化如深色模式后重建数据 val data remember(isDarkTheme) { buildThemeData(isDarkTheme) }⚠️ 注意rememberSaveable用 inputs 做 key 时要小心参见下方对比表。4. 常见用途kotlinremember { mutableStateOf(0) } // 状态 remember { derivedStateOf { list.filter { ... } } } // 派生状态 rememberCoroutineScope() // 协程作用域 rememberLauncherForActivityResult(...) // Activity Result rememberScrollState() // 滚动状态 LaunchedEffect(Unit) { } // 副作用也是一种 remember 机制四、byKotlin 委托属性这行代码用了 Kotlin 的委托属性Delegated Propertieskotlinvar count by remember { mutableStateOf(0) } // 等价于 val countState remember { mutableStateOf(0) } var count: Int get() countState.value set(value) { countState.value value }编译器会调用读count→getValue(this, ::count)写count→setValue(this, ::count, value)MutableState通过扩展函数提供了这两个操作符kotlinoperator fun T StateT.getValue(thisObj: Any?, property: KProperty*): T value operator fun T MutableStateT.setValue(thisObj: Any?, property: KProperty*, value: T) { this.value value }需要导入kotlinimport androidx.compose.runtime.getValue import androidx.compose.runtime.setValue 用var ... by而非val ... 的好处代码更简洁但把 State 传给子组合时需要by remember { ... }持有者模式见下文状态提升。五、完整示例kotlinimport androidx.compose.runtime.* Composable fun Counter() { // 1. 声明状态remember 缓存 mutableStateOf 可观察 var count by remember { mutableStateOf(0) } Column { // 2. 读取 → 建立依赖 Text(点击次数: $count) // 3. 写入 → 触发重组 Button(onClick { count }) { Text(点我 1) } } }执行流程首次组合: remember 创建 state(0) → 显示 0 点击按钮: count 1 → snapshot 通知 → 调度重组 重组: remember 返回缓存的 state → 显示 1 再次点击: count 2 → 重组 → 显示 2六、进阶相关状态 API 对比API跨重组保留跨配置变化保留用途remember { mutableStateOf() }✅❌如旋转屏幕会丢普通 UI 状态rememberSaveable { mutableStateOf() }✅✅存入Bundle需要 survives 配置变化/进程重建的状态derivedStateOf { }依赖其他 state❌从其他状态计算派生值mutableStateListOf / mutableStateMapOf✅❌可观察的集合collectAsState()/collectAsStateWithLifecycle()✅❌Flow → Compose 状态produceState { }✅❌异步加载数据到状态1.rememberSaveablekotlin// 旋转屏幕 / 进程被杀重建后仍保留 var count by rememberSaveable { mutableStateOf(0) }⚠️ 局限只能存Bundle支持的类型基本类型、Parcelable 等。2.derivedStateOf派生状态当状态变化频率高、但 UI 只需关注变化后的结果时使用避免过度重组kotlinval query by remember { mutableStateOf() } // 只有 isEmpty 的结果变化时才触发重组而不是每次 query 变化 val isQueryEmpty by remember { derivedStateOf { query.isEmpty() } }3. 可观察集合kotlinval items remember { mutableStateListOfString() } items.add(A) // ✅ 触发重组 // 普通 mutableListOf remember 不会触发重组七、常见坑坑 1忘记rememberkotlinvar count mutableStateOf(0) // ❌ 每次重组重置为 0坑 2在非组合作用域读写 statekotlinButton(onClick { count }) // ✅ 事件回调中写入没问题 Text($count) // ✅ 组合中读取没问题读取必须在组合或 snapshot observer中进行否则无法建立依赖。坑 3状态应该放在哪里—— 状态提升State Hoisting如果一个状态被多个组合共享、或需要业务逻辑控制应该上移到调用方kotlin// 无状态stateless的可复用组合 Composable fun Counter(count: Int, onCountChange: (Int) - Unit) { Button(onClick { onCountChange(count 1) }) { Text(count: $count) } } // 状态持有者 Composable fun CounterScreen() { var count by remember { mutableStateOf(0) } Counter(count count, onCountChange { count it }) }好处可预览、可测试、状态来源单一。坑 4remember的 key 用不稳定对象kotlinval user User(name Tom) remember(user) { ... } // ⚠️ user 每次重组都是新对象 → 缓存永远失效应使用稳定的 key如user.id。坑 5把remember当性能优化工具到处用remember缓存的是对象创建对纯计算用remember(key)但读 state 本身不需要 remember。滥用remember(expensive)可能反而引入错误 key 的 bug。八、一句话总结mutableStateOf负责变了能通知可观察remember负责重组时不丢缓存by负责写起来简洁Kotlin 委托。三者结合就是 Compose 声明式 UI 的状态基石你只管改数据界面自动刷新。Composeremember与mutableStateOf原理详解一、整体架构概览Compose 的响应式系统由三层构成text┌─────────────────────────────────────────┐ │ SlotTable槽表 - 存储 remember 值 │ ├─────────────────────────────────────────┤ │ Snapshot快照系统 - 状态读写追踪 │ ├─────────────────────────────────────────┤ │ Recomposer重组器 - 调度重组 │ └─────────────────────────────────────────┘核心流程读状态时记录观察者 → 写状态时通知观察者 → Recomposer 触发重组。二、remember原理1. SlotTable槽表remember依赖 Compose 编译期的位置记忆化Positional Memoization背后是SlotTable数据结构。SlotTable本质是两个并行数组groups: IntArray— 记录 group 的 key、size、状态标志slots: ArrayAny?— 实际存储的数据每个Composable函数在编译期会被包成startRestartGroup()/endRestartGroup()kotlinComposable fun MyComposable() { val composer currentComposer composer.startRestartGroup(0x1234) // 分配 group // ... body composer.endRestartGroup() }2.remember的实现kotlinComposable inline fun T remember( crossinline calculation: DisallowComposableCalls () - T ): T currentComposer.cache(false, calculation)ComposerImpl.cache的精简逻辑kotlinoverride fun T cache(invalidate: Boolean, calculation: () - T): T { val slot nextSlot() // 移动 slot 指针取当前位置 return if (invalidate || slot EMPTY) { val value calculation() // 首次执行 lambda updateSlot(slot, value) // 写回槽表 value } else { Suppress(UNCHECKED_CAST) slot as T // 后续直接复用 } }3. 关键点位置决定身份每次调用remember都会按执行顺序消费一个 slotkotlinComposable fun Counter() { val a remember { 1 } // slot[0] val b remember { 2 } // slot[1] }重组时只要调用顺序不变slot 指针就能匹配到相同位置读到之前存的值。⚠️ 这就是为什么不能在if/for中调用remember和 Composable位置会漂移导致 slot 错位读到的可能是别的值。4. 带 key 的 rememberkotlinComposable inline fun T remember(key1: Any?, calculation: () - T): T { val composer currentComposer val invalidate composer.changed(key1) // key 变化返回 true return composer.cache(invalidate, calculation) }composer.changed(key)会把 key 存进槽表并与上次比较不等则返回 true → 触发重新计算。三、mutableStateOf原理1. 创建kotlinfun T mutableStateOf( value: T, policy: SnapshotMutationPolicyT structuralEqualityPolicy() ): MutableStateT createSnapshotMutableState(value, policy)返回的是ParcelableSnapshotMutableState/SnapshotMutableStateImplAndroid 上是 Parcelable 版本。2. SnapshotMutableStateImpl 核心结构kotlinprivate class SnapshotMutableStateImplT( value: T, override val policy: SnapshotMutationPolicyT ) : StateObject, MutableStateT { private var next: StateStateRecordT StateStateRecord(value) override var value: T get() next.readable(this).value set(newValue) next.withCurrent { current - if (!policy.equivalent(current.value, newValue)) { next.overwritable(this, current) { this.value newValue } } } }关键真正的值存在StateRecord链表中next指向当前快照下的有效记录。3. StateRecord快照隔离的基石每个 StateRecord 有kotlinabstract class StateRecord { var snapshotId: Int INVALID_SNAPSHOT var next: StateRecord? null // 链表 }当不同 Snapshot 修改同一个 State 时会分叉成多条 RecordtextGlobal Snapshot ──▶ Record1(value1) Snapshot A ──▶ Record2(value2) // A 的修改 Snapshot B ──▶ Record3(value3) // B 的修改readable(this)会遍历链表找到当前快照可见的最新 Record。这实现了快照隔离不同快照读到不同值互不干扰。4. 默认使用 GlobalSnapshot日常代码里state.value读写走的是Snapshot.current通常是GlobalSnapshot所以是立即可见的。四、读取追踪readObserver1. 重组作用域RecomposeScopeImpl每个 Composable 在编译后都有一个RecomposeScopeImpl它负责记录「我读了哪些 State」当这些 State 变化时把自己标记为「无效 → 需要重组」2. 读取时如何记录当 Composable 执行state.value时textstate.value │ ▼ Snapshot.current.readObserver?.invoke(state) │ ▼ Recomposer 在组合期间设置的 observer │ ▼ currentRecomposeScope.recordRead(state) │ ▼ state.recordRead(reader) → 把 scope 注册到 State 的观察者列表精简版kotlin// RecomposeScopeImpl override fun recordRead(state: StateObject) { // 只有当读取发生在当前正在组合的 scope 内才记录 observations?.add(state) // 存到 scope 的观察集 state.recordRead(this) // 反向State 也持有 observer 引用 }Composer 在组合前会设置观察器kotlin// 组合开始时 Snapshot.observe(readObserver { state - currentScope.recordRead(state) })五、写入与失效1. 写入路径kotlinstate.value newValue │ ▼ withCurrent { current - if (!policy.equivalent(current.value, newValue)) { overwritable(this, current) { this.value newValue } } } │ ▼ 创建/复用 StateRecord写入新值 │ ▼ notifyObservers / 标记失效 │ ▼ 所有注册的 RecomposeScopeImpl 被 invalidate() │ ▼ Recomposer 通过 MonotonicFrameClock 调度重组2. 结构相等策略默认structuralEqualityPolicy()会用比较值相同就不触发重组kotlinval s mutableStateOf(0) s.value 0 // 相等不触发失效 s.value 1 // 不等触发其他策略referentialEqualityPolicy()— 用neverEqualPolicy()— 永远不等每次都触发3. Apply Observer全局快照提交时通知kotlin// Recomposer 初始化时 Snapshot.registerApplyObserver { changed, snapshot - // 找到受影响的 RecomposeScope标记 invalid ... recomposer.scheduleRecompose() }六、完整的一次状态更新流程kotlinComposable fun Counter() { val count remember { mutableStateOf(0) } Button(onClick { count.value }) { Text($count) // ① 读取 count.value } }首次组合remember在 SlotTable 中创建SnapshotMutableStateImpl并存储。读取count.valueText所在 scope 被注册为该 State 的观察者。点击按钮count.value→ 写入新 Record → 通知观察者。Recomposer 调度通过 frame clock 安排下一帧重组。重组只重组Text所在的最小 scope。再次读取remember从槽表拿到同一个 State 实例读到新值UI 刷新。七、几个常见问题的答案Q1为什么 recomposition 后 remember 的值不会丢因为它存在SlotTable里而不是存在局部变量栈上。重组只是重新执行 Composable 函数体remember会从槽表里取回上次的值。Q2为什么remember在if里会出错kotlinif (cond) { val a remember { ... } // 位置不稳定 } val b remember { ... } // 位置会被 a 挤占SlotTable 是按调用顺序对齐的条件分支会让 slot 指针错位导致b读到a的 slot。解决用remember(key) {}或key(cond) { ... }显式提供位置标识。Q3Snapshot 系统为什么这么复杂Record 链表为了支持事务性修改Snapshot.takeMutableSnapshot()可以隔离修改apply()才提交。多线程并发不同线程用不同快照互不阻塞。Compose 的原子性一次重组期间看到的状态是一致的快照。Q4by委托是怎么回事kotlinvar count by remember { mutableStateOf(0) }编译期转成kotlinval state remember { mutableStateOf(0) } var count: Int get() state.value set(v) { state.value v }读写仍然走getValue/setValue追踪机制完全一样。八、总结对比组件职责关键数据结构remember跨重组保留值SlotTable数组 位置指针mutableStateOf可观察状态StateRecord 链表 SnapshotRecomposeScope最小重组单元观察者集合Recomposer调度重组失效队列 FrameClockSnapshot隔离与原子性SnapshotId Record 链表一句话总结remember靠「槽表 位置」保留对象mutableStateOf靠「快照 Record 链表」保存值读时把当前RecomposeScope注册为观察者写时通知它失效Recomposer再精确地只重组这一个 scope。这就是 Compose 能做到细粒度、可预测、高性能重组的根本原因。深入篇重组跳过机制、Snapshot 并发隔离、produceState一、重组的细粒度跳过机制Recomposition Skipping1. 什么是跳过重组不等于整个界面重画。Compose 会只重新执行状态真正影响到的组合函数其余直接跳过。kotlinComposable fun Screen() { var count by remember { mutableStateOf(0) } var name by remember { mutableStateOf(Tom) } Column { Counter(count) // count 变化 → 重组 Greeting(name) // count 变化时Greeting 会被跳过 } }当count变化时plainScreen 重组 ├─ Counter(count) → 重新执行读到了新 count ├─ Greeting(name) → 跳过参数 name 没变化且函数内没有读变化的 state └─ Column 内部布局 → 按需重测2. 跳过生效的条件一个组合函数能被跳过需要满足条件说明函数没有读取发生变化的 state依赖追踪决定谁需要重组所有参数都满足equals 相等编译器用equals对比参数函数没有副作用依赖执行稳定的非稳定类型会破坏跳过3. 关键概念稳定性Stability跳过判断的核心是参数的类型稳定性稳定类型Stableequals结果不会随时间变且 public 属性变化会通知 Compose。如Int、String、接口类型、被Stable/Immutable注解的类不稳定类型Unstable如普通var属性的自定义 classkotlin// ❌ 不稳定每次 Screen 重组传入新实例 // equals 结果可能不同 → UserCard 无法被跳过 class User(var name: String, var age: Int) Composable fun Screen() { var count by remember { mutableStateOf(0) } UserCard(User(Tom, 20)) // 每次 Screen 重组都新建 User }修复方式kotlin// ✅ 方式1Stable 注解承诺变化会通过 state 通知 Stable class User(var name: String, var age: Int) // ✅ 方式2Immutable承诺创建后不可变 Immutable data class User(val name: String, val age: Int) // ✅ 方式3用 remember 缓存实例 val user remember { User(Tom, 20) } UserCard(user)注意对于data classval 属性Compose 编译器能自动推断稳定性带var的普通类必须手动注解。4.CompositionLocal的跳过规则CompositionLocal也是隐式参数读取了某个 Local 的组合会在其变化时重组kotlinval isDark LocalDarkTheme.current // LocalDarkTheme 变化 → 重组这与显式传参二选一参数 vs CompositionLocal是 Compose 架构的经典权衡——显式参数利于测试和跳过Local 利于跨层传递。5. 反向确认什么情况下会被跳过失效kotlinComposable fun Bad(count: Int) { val context LocalContext.current // 读取了 context隐式参数context 变化时即使 count 不变也会重组 }还有inline fun组合inline 后无法跳过、读取不稳定类型等。二、Snapshot 的并发隔离1. Snapshot 不只是通知它是一个 MVCC 事务系统Compose 的状态系统底层是Snapshot快照设计目标有二细粒度依赖追踪谁读了 state值变了通知谁并发隔离多线程读写 state 时不互相污染它类似数据库的MVCC多版本并发控制线程 A 修改 count: 1 → 2 → 在 A 的快照中 count2 线程 B 此刻读 count → 在 B 的快照中 count 仍是 1隔离 A 提交快照 → 全局 state 变为 2B 下次读取看到 22. 核心 API快照内的修改不会立即全局生效kotlinval state mutableStateOf(0) val snapshot Snapshot.takeSnapshot() try { snapshot.enter { state.value 100 // 在这个快照里改成 100 println(state.value) // 100 } println(state.value) // 仍然是 0还没提交 snapshot.apply() // 提交全局生效 println(state.value) // 100 } finally { snapshot.dispose() }如果在enter块中崩溃直接dispose()即可丢弃修改——相当于事务回滚。并行修改冲突两个快照同时改同一个 state后提交的会失败乐观并发控制kotlinval s1 Snapshot.takeSnapshot() val s2 Snapshot.takeSnapshot() s1.enter { state.value 10 } s2.enter { state.value 20 } s1.apply() // ✅ 成功全局 state 10 s2.apply() // ❌ 失败抛出异常或返回 false // 因为 s2 基于的旧值已被 s1 改掉3. Snapshot 与重组的关系重组其实运行在一个快照观察者环境里plain组合函数执行时读取 state → 记录依赖Snapshot 观察者 state 变化 快照系统比对 → 通知观察者 → 调度重组这就是为什么只能在组合/观察者环境中读取 state才能建立依赖在LaunchedEffect等副作用里读 state 并不会触发重组需要用snapshotFlow { }桥接kotlinLaunchedEffect(Unit) { // ✅ 把 state 转成 Flowstate 变化 → Flow 发射 → 副作用响应 snapshotFlow { query } .debounce(300) .collect { result - search(result) } }4. 线程安全性mutableStateOf的读写是线程安全的内部有同步机制但注意kotlin// ✅ 可以后台线程改 stateCompose 会自动调度重组 scope.launch(Dispatchers.IO) { count 5 }不需要切回主线程Snapshot 系统保证一致性且 Compose 会智能地把重组调度到 UI 线程。三、produceState异步加载数据到状态1. 解决什么问题remember { mutableStateOf() }适合同步初始化的数据但数据来自网络、数据库、回调时需要异步加载——produceState就是为这个场景设计的kotlinComposable fun UserScreen(userId: String) { // 返回一个 StateUser?加载中/完成/失败都能表达 val user by produceStateUser?(initialValue null, userId) { value repository.fetchUser(userId) // value 就是 MutableState.value } when { user null - Loading() else - UserCard(user!!) } }2. 等价展开理解原理produceState本质是一个语法糖kotlin// 伪代码展开 Composable fun T produceState(initialValue: T, key: Any?, producer: suspend ProduceStateScopeT.() - Unit): StateT { val result remember(key) { mutableStateOf(initialValue) } LaunchedEffect(key) { ProduceStateScopeImpl(result).producer() } return result }所以记住它的三个特性特性说明remember(key)key 变化如userId变了→ 重置为初始值并重新加载LaunchedEffect(key)加载逻辑在协程里执行可挂起作用域value可直接赋值也可try/catch处理异常3. 完整实战模式kotlinsealed interface UiStateout T { data object Loading : UiStateNothing data class SuccessT(val data: T) : UiStateT data class Error(val message: String) : UiStateNothing } Composable fun T rememberData( key: Any?, block: suspend () - T ): StateUiStateT produceStateUiStateT(initialValue UiState.Loading, key) { value try { UiState.Success(block()) } catch (e: Exception) { UiState.Error(e.message ?: 未知错误) } } // 使用 Composable fun RepoScreen(repoId: String) { val state by rememberData(repoId) { repository.getRepo(repoId) } when (val s state) { UiState.Loading - LoadingView() is UiState.Error - ErrorView(s.message) is UiState.Success - RepoCard(s.data) } }4.produceStatevs 其他方案方案适用场景produceState组合内的一次性异步加载无 ViewModelViewModelcollectAsStateWithLifecycle()有业务层、需跨组合存活生产环境首选LaunchedEffectmutableStateOf手动版 produceState需要更精细控制时collectAsState(initial)把已有 Flow 直接转 Statekotlin// 生产环境典型写法ViewModel 持有状态流 Composable fun RepoScreen(viewModel: RepoViewModel viewModel()) { val state by viewModel.uiState.collectAsStateWithLifecycle() // ... }produceState适合简单页面、预览、或没有架构层的场景一旦状态需要跨页面共享、需要缓存策略、需要处理复杂生命周期就交给 ViewModel。四、三者串起来一条完整的数据流① produceState / snapshotFlow 负责数据怎么来 ↓ ② mutableStateOf remember 负责数据怎么存、谁能看见 ↓ (Snapshot 追踪依赖变化自动通知) ③ 重组调度 负责谁需要刷新 ↓ (稳定性 参数对比) ④ 细粒度跳过 负责只刷新该刷新的部分一句话总结Snapshot 是地基并发安全 依赖追踪跳过机制是性能优化稳定性 equalsproduceState是异步数据入口remember LaunchedEffect 的组合糖。