卷 VI · 落地CH 24深度 24/24
出了问题,从哪一格开始查
这是最后一章,也是唯一一章你不需要读完的——它的用法是遇到问题的时候翻回来,对着第二列问自己一句。二十四章的内容,最后能留下的就是这十二句。
那张表
用法不是背,是看第二列。每一句「先问自己」都是一个能在三十秒内验证的判断,而不是一个需要重构才能验证的猜想。
它们其实是同一个问题
把那十二行的第二列排在一起,你会发现它们在问同一件事的不同侧面:
「你是不是就地改了同一个对象?」 → 它没变,所以什么都没重跑 「你在最外层读了一个高频 state 吗?」 → 太多东西重跑了 「有没有写 key?」 → 该活下来的没活下来 「是不是两个地方 collect 了同一个冷流?」 → 该共享的重跑了两遍 「那个循环里有挂起点吗?」 → 该取消的没被取消 「用的是 collectAsState 还是 WithLifecycle?」 → 该停的没停 「事件是不是塞进 State 了?」 → 该消失的活下来了 「key 里带上那个参数了吗?」 → 该重跑的没重跑
八句话,三个词:
| 做对了是 | 做错了是 | 典型症状 | |
|---|---|---|---|
| 活下来 | 该记的记住了 | 该记的没记住 / 不该记的记住了 | 状态莫名其妙没了 / 退出重进还在 |
| 重跑 | 只有该重跑的重跑了 | 多跑了 / 少跑了 | 卡顿 / 界面不更新 |
| 取消 | 该停的停了 | 没停 / 停错了时候 | 后台耗电 / 请求发两遍 / 事件丢了 |
这本书从第 1 章到第 23 章,讲的全部是这三个词在三套机制里的表现。
三套机制的同一张骨架
| Compose | 协程 | Flow | |
|---|---|---|---|
| 一次「跑」是什么 | 一次 composable 调用 | 一段到下一个挂起点为止的代码 | 一次 collect,把配方走一遍 |
| 什么活下来 | slot 表里 remember 的那一格 | 状态机的 L$ 字段 | replay 缓存、StateFlow.value |
| 什么重算 | 被标脏的那个 scope | resumeWith 之后从 label 接着跑 | 每次 collect 都从头生产 |
| 什么被取消 | 离开组合的那一段(effect 跟着走) | cancel() + 你在挂起点上检查 | 订阅归零停上游、collectLatest 掐上一个 |
| 身份怎么定 | 位置 + 函数 + key | Job 在树上的位置 | 谁在 collect |
| 「不变」怎么判定 | equals/引用(第 4 章) | — | distinctUntilChanged、StateFlow 自带去重 |
这张表值得多看两眼。三列的行为如此一致,不是巧合——它们都在解决同一个问题:在一个「代码会被反复执行」的世界里,怎么管理身份、状态和生命周期。
Compose 的答案是 slot 表,协程的答案是状态机 + Job 树,Flow 的答案是「配方 + 收集者」。三种答案,同一个问题。
一份可以贴在墙上的清单
如果只能留下十条,我会留这十条:
- composable 的 body 里不要有副作用。它跑几次、跑不跑,你都不知道。(第 18 章)
- 状态里只放不可变类型。
List不是MutableList,val不是var,改就用copy()。(第 5、22 章) - 高频 state 传 lambda,不传值。这是最省力的一条优化。(第 6 章)
- 列表一定写
key。它不是给你排序用的,是给 Compose 认人用的。(第 22 章) collectAsStateWithLifecycle+WhileSubscribed(5000),两个成对。(第 13、19 章)- effect 块里读到的外部值都要在 key 里。不想重启就用
rememberUpdatedState。(第 17 章) - 纯 CPU 的长循环里加
ensureActive()。不加就是「取消了但没停」。(第 11 章) - 主线程安全是函数自己的责任。
suspend函数应该在内部withContext,不要求调用方记得。(第 12 章) - 不要
catch (e: Exception)而不放回CancellationException。(第 11 章) - 先量再改。用 debug 构建找问题,用 release 构建下结论。(第 23 章)
◆ 全书的一句话
你写的不是界面,是一个会被反复调用的函数。
你要操心的从来不是「怎么跑一遍」——那部分声明式 UI 已经替你做好了,而且做得比你手写好。
你要操心的是重跑的时候:什么活下来、什么重算、什么被取消。
这三个问题,Compose 问一遍,协程问一遍,Flow 再问一遍。答对了,这三套东西就是一套;答错了,你会得到三种看起来毫不相干、其实同源的 bug。
还剩下什么没讲
诚实地说一下这本书的边界:
- 动画——
animate*AsState、Transition、AnimatedContent。第 2 章讲了它们和三阶段的关系,但动画本身的 API 面很大,没展开。 - 自定义布局和绘制——
Layout、SubcomposeLayout、Canvas、Modifier.Node。这些属于「怎么画」,不属于「什么时候跑」。 - 手势——
pointerInput里那套挂起式的手势 DSL 其实是协程用得最漂亮的地方之一,值得单独一本。 - 测试——
ComposeTestRule、runTest、TestDispatcher、Turbine。《证伪》那本书讲的就是这个。 - 多平台——Compose Multiplatform 在 iOS 和桌面上的差异。
还有两块被有意放在别处:Kotlin 语言本身去 《坩埚》,项目架构去 《承重》。
⇄ 接下来读什么
最后一件事
如果你合上这本书之后只记得一个动作,我希望是这个:
下次写完一个 composable,在提交之前,问自己一句:
「我刚才写的这些,会在什么时候被再跑一遍?」
答得上来,你就不需要这本书了。
这一章的一句话
十二个症状,十二句「先问自己」,问的都是同一件事的一个侧面:这一次,什么活下来了、什么重算了、什么该被取消却没有。