卷 VI · 落地CH 24深度 24/24

出了问题,从哪一格开始查

这是最后一章,也是唯一一章你不需要读完的——它的用法是遇到问题的时候翻回来,对着第二列问自己一句。二十四章的内容,最后能留下的就是这十二句。

决策表自检清单三个动词

那张表

用法不是背,是看第二列。每一句「先问自己」都是一个能在三十秒内验证的判断,而不是一个需要重构才能验证的猜想。

它们其实是同一个问题

把那十二行的第二列排在一起,你会发现它们在问同一件事的不同侧面:

「你是不是就地改了同一个对象?」          → 它没变,所以什么都没重跑
「你在最外层读了一个高频 state 吗?」      → 太多东西重跑了
「有没有写 key?」                        → 该活下来的没活下来
「是不是两个地方 collect 了同一个冷流?」  → 该共享的重跑了两遍
「那个循环里有挂起点吗?」                → 该取消的没被取消
「用的是 collectAsState 还是 WithLifecycle?」 → 该停的没停
「事件是不是塞进 State 了?」             → 该消失的活下来了
「key 里带上那个参数了吗?」              → 该重跑的没重跑

八句话,三个词:

做对了是做错了是典型症状
活下来该记的记住了该记的没记住 / 不该记的记住了状态莫名其妙没了 / 退出重进还在
重跑只有该重跑的重跑了多跑了 / 少跑了卡顿 / 界面不更新
取消该停的停了没停 / 停错了时候后台耗电 / 请求发两遍 / 事件丢了

这本书从第 1 章到第 23 章,讲的全部是这三个词在三套机制里的表现。

三套机制的同一张骨架

Compose协程Flow
一次「跑」是什么一次 composable 调用一段到下一个挂起点为止的代码一次 collect,把配方走一遍
什么活下来slot 表里 remember 的那一格状态机的 L$ 字段replay 缓存、StateFlow.value
什么重算被标脏的那个 scoperesumeWith 之后从 label 接着跑每次 collect 都从头生产
什么被取消离开组合的那一段(effect 跟着走)cancel() + 你在挂起点上检查订阅归零停上游、collectLatest 掐上一个
身份怎么定位置 + 函数 + keyJob 在树上的位置谁在 collect
「不变」怎么判定equals/引用(第 4 章)distinctUntilChanged、StateFlow 自带去重

这张表值得多看两眼。三列的行为如此一致,不是巧合——它们都在解决同一个问题:在一个「代码会被反复执行」的世界里,怎么管理身份、状态和生命周期。

Compose 的答案是 slot 表,协程的答案是状态机 + Job 树,Flow 的答案是「配方 + 收集者」。三种答案,同一个问题。

一份可以贴在墙上的清单

如果只能留下十条,我会留这十条:

  1. composable 的 body 里不要有副作用。它跑几次、跑不跑,你都不知道。(第 18 章)
  2. 状态里只放不可变类型。List 不是 MutableListval 不是 var,改就用 copy()。(第 5、22 章)
  3. 高频 state 传 lambda,不传值。这是最省力的一条优化。(第 6 章)
  4. 列表一定写 key它不是给你排序用的,是给 Compose 认人用的。(第 22 章)
  5. collectAsStateWithLifecycle + WhileSubscribed(5000),两个成对。(第 13、19 章)
  6. effect 块里读到的外部值都要在 key 里。不想重启就用 rememberUpdatedState。(第 17 章)
  7. 纯 CPU 的长循环里加 ensureActive()不加就是「取消了但没停」。(第 11 章)
  8. 主线程安全是函数自己的责任。suspend 函数应该在内部 withContext,不要求调用方记得。(第 12 章)
  9. 不要 catch (e: Exception) 而不放回 CancellationException(第 11 章)
  10. 先量再改。用 debug 构建找问题,用 release 构建下结论。(第 23 章)
◆ 全书的一句话

你写的不是界面,是一个会被反复调用的函数。

你要操心的从来不是「怎么跑一遍」——那部分声明式 UI 已经替你做好了,而且做得比你手写好。

你要操心的是重跑的时候:什么活下来什么重算什么被取消

这三个问题,Compose 问一遍,协程问一遍,Flow 再问一遍。答对了,这三套东西就是一套;答错了,你会得到三种看起来毫不相干、其实同源的 bug。

还剩下什么没讲

诚实地说一下这本书的边界:

  • 动画——animate*AsStateTransitionAnimatedContent。第 2 章讲了它们和三阶段的关系,但动画本身的 API 面很大,没展开。
  • 自定义布局和绘制——LayoutSubcomposeLayoutCanvasModifier.Node。这些属于「怎么画」,不属于「什么时候跑」。
  • 手势——pointerInput 里那套挂起式的手势 DSL 其实是协程用得最漂亮的地方之一,值得单独一本。
  • 测试——ComposeTestRulerunTestTestDispatcherTurbine《证伪》那本书讲的就是这个。
  • 多平台——Compose Multiplatform 在 iOS 和桌面上的差异。

还有两块被有意放在别处:Kotlin 语言本身《坩埚》项目架构《承重》

最后一件事

如果你合上这本书之后只记得一个动作,我希望是这个:

下次写完一个 composable,在提交之前,问自己一句:
「我刚才写的这些,会在什么时候被再跑一遍?」

答得上来,你就不需要这本书了。

这一章的一句话

十二个症状,十二句「先问自己」,问的都是同一件事的一个侧面:这一次,什么活下来了、什么重算了、什么该被取消却没有。