幕 II · 拆除与新建CH 02进度 02/06

你错过的 18 个月

这一章是一份清单,不是一篇散文。我把 2024 年中到 2026 年中之间的变化筛过一遍,扔掉了所有「知道了也不影响你写代码」的东西,只留下六条真的会改变你手指怎么敲的。每条都标了它属于哪一类:该立刻上(不上就是在给自己挖坑)、该知道(不一定用得上,但别人聊起来你得懂)、可以先不管(热闹,但不急)。

编译器并入 Kotlin强跳过@Immutable 可以删了多平台化targetSdk 36

一 · Compose 编译器搬进了 Kotlin

该立刻上  Kotlin 2.0 起

你还记得那张版本对照表吗?Compose 编译器扩展版本必须和 Kotlin 版本严格配对,升一个 Kotlin 小版本 要先去查表确认编译器有没有跟上 —— 没跟上就得等,一等好几周。

那张表没了。Compose 编译器从 Kotlin 2.0 起并入了 Kotlin 仓库,跟着 Kotlin 一起发版。

// 以前(现在会报错):
android {
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.14"   // ← 得去查表
    }
}

// 现在:它就是个普通的 Kotlin 编译器插件
plugins {
    alias(libs.plugins.kotlin.compose)   // org.jetbrains.kotlin.plugin.compose
}
android {
    buildFeatures { compose = true }
}
◆ 判词 · 这不只是省了一行配置

真正的意义是:Compose 不再是个「挂在 Kotlin 外面的第三方东西」,它是 Kotlin 工具链的一部分。 Kotlin 出新版本,Compose 当天就能用。这也是后面所有事情(多平台、编译器插件式 DI)的前提。

二 · 强跳过默认开启 —— 你可以删掉那些 @Immutable 了

该立刻上  Kotlin 2.0.20+ 默认

这条是本章最实惠的一条。先回忆一下你离开前的痛:Compose 的「稳定性」

当年的规则很硬:一个 composable 想被跳过(参数没变就不重画),它的所有参数都得是稳定的。 而 ListSetMap 是接口 —— 编译器无法保证它们不会被偷偷改掉 —— 所以统统判定为不稳定。 结果:一个参数里带 List 的 composable,整个函数「不可跳过」,父一重组它必重组,连带整棵子树跟着重画。

于是我们发明了一堆体操:给 data class 打 @Immutable、引入 kotlinx-collections-immutableImmutableList、把 lambda 用 remember 包起来……

强跳过(strong skipping)把这一整套体操废掉了。它默认开启,你什么都不用做。它干了两件事:

  • 不稳定参数改用「实例比较」。以前不稳定 = 不可跳过;现在是:比一比这个 List 还是不是同一个对象(===)。是,就跳过。
  • lambda 自动记忆化。你写在参数里的 { ... },编译器自动帮你 remember 了,不会每次重组都造一个新对象。

下面这个可视化,拿同一棵 composable 树、同一个改动,在三种规则下跑一遍,把重组的节点数数给你看。 先看默认的「改 title」:老规则重组 6 个节点,新规则只重组 2 个。

▣ 你现在可以删掉的东西
  • 绝大多数 @Immutable / @Stable 注解 —— 加了也不再有额外收益。
  • 为了稳定性而引入的 ImmutableList / PersistentList —— 普通 List 现在够用了。 (如果你是为了真的不可变语义而用它,那另说,继续用。)
  • 手工把 lambda remember { } 起来的那些代码。

删之前跑一遍 Compose 编译器的指标报告确认一下,但大概率你会删掉一大片。

⚠ 但有一个坑,强跳过救不了你

在上面的 Demo 里切到「收藏第 2 篇」,你会看到:强跳过模式下,三张卡片全部重组了 —— 可只有第 2 张的内容真的变了。重组数和老规则一样是 5。

凶手是那个 lambda。{ id -> vm.toggle(id, id in bookmarks) } 捕获了 bookmarks。 编译器确实帮你记忆化了它,但 bookmarks 一变,这个记忆化的 lambda 就得重建 —— 于是每张卡片都看到「我的 onBookmark 参数变了」,全部重组。

修法在 Demo 的第三个选项里:用方法引用 vm::toggle,不捕获任何东西。 重组数立刻从 5 掉到 3(只有真正变了的那张卡重组)。

规律:强跳过消灭了「类型不稳定」这个问题,但没有消灭「lambda 捕获」这个问题。 列表项的回调,尽量别在里面捕获会变的东西。

三 · 导航:一次跳了两级

该立刻上  Nav 2.8(2024-08) → Nav3(2025-11 转正)

你离开的时候,导航还是这样写的:

// 2024 年上半年:字符串路由
navController.navigate("detail/${article.id}")

composable("detail/{id}") { backStackEntry ->
    val id = backStackEntry.arguments?.getString("id")   // 手动挖参数,类型全靠祈祷
    DetailScreen(id!!)
}

然后发生了两件事,而且是接连发生的:

"detail/{id}" 字符串路由 @Serializable data class Detail(val id: String) Nav 2.8 · 2024-08
类型安全路由。你刚走没多久就出的,所以你可能压根没见过它。
NavHost 替你保管返回栈 返回栈就是你手里的一个 List Nav3 · 2025-11 转正
推倒重来。整个心智模型都换了 —— 这是下一整章的内容。
◆ 判词 · 好消息:你可以跳过中间那一级

如果你的项目还在用字符串路由,不必先迁到 Nav 2.8 的类型安全路由、再迁到 Nav3。 Nav3 是完全不同的一套 API,中间那一级对你没有任何过渡价值。直接看第 3 章,一步到位。

四 · 生命周期组件全面多平台化

该知道  2024–2025 陆续

这条你不一定用得上,但它是理解「为什么社区在往那个方向走」的钥匙。这一年半里, Jetpack 把一批核心库搬进了 Kotlin Multiplatform:

现在的样子意味着什么
lifecycle-viewmodel 多平台 commonMain 里可以放一个真的 ViewModel,连 viewModelScopeSavedStateHandle 都在
Room 多平台 同一份 DAO 和 @Entity,Android / iOS / Desktop 通吃
DataStore 多平台 偏好存储也下沉了
Coil 3 多平台 图片加载 —— Mihon 已经在用 3.5
navigation3-runtime 多平台 连导航都能共享了
◆ 判词 · 这不是「Android 库能跑在 iOS 上」那么简单

真正的后果是:第 1 章那三层,现在可以整体住进 commonMain。数据层、状态持有者、 甚至 UI 层(Compose Multiplatform),都能一次写完两端用。

Compose 的 iOS 支持自 1.8.0(2025 年 5 月)起标记为稳定,如今已到 1.10.x。 但要泼一盆冷水:iOS 的无障碍(accessibility)支持至今仍是公认的短板。 做医疗、金融、政务这类有合规要求的 app,上 CMP 之前务必先把这条摸清楚。

五 · Material 3 Expressive

可以先不管  2025 年 I/O 发布,组件陆续转正

新的一套设计语言:更大胆的形状、更有个性的动效,以及一批新组件(FloatingToolbar、FAB 菜单、 新的加载指示器等)。它们正在从实验性 API 逐步转正。

为什么归到「可以先不管」:它是视觉层的事,不影响架构。你的分层、状态流、导航一行都不用改。 等设计师找上门时再学不迟。但别把它和 Material 3 搞混 —— M3 本身早就是标配了。

六 · 平台侧:AGP 9、targetSdk 36 和两条硬性要求

该立刻上  Android 16

这两条是平台强制的,和你想不想升级无关 —— Play 商店的 targetSdk 门槛会推着你走:

  • edge-to-edge 全面强制。Android 15 时还留了个临时的 opt-out 开关,到 Android 16 那个开关 不再被尊重。你的内容一定会画到状态栏和导航栏底下。 所以 WindowInsets 不再是「适配刘海屏」的可选功课,而是必修课 —— 该加 Modifier.safeDrawingPadding() 的地方一个都不能少,否则按钮就被系统手势条盖住了。
  • 大屏不能再锁方向。在大屏设备上(可用宽度 ≥ 600dp),你在 manifest 里写的 screenOrientationresizableActivity 会被忽略。 「我们是手机 app,锁死竖屏就行」这个偷懒选项,在平板和折叠屏上没有了。 这直接把「自适应布局」从加分项变成了必答题 —— 这也正是 Navigation 3 把 多窗格做进核心 API 的原因(第 3 章)。

构建侧顺带一提:Now in Android 现在跑在 AGP 9.0 + Kotlin 2.3 上, Mihon 更激进,已经上了 Kotlin 2.4。K2 编译器早就是默认了。

清单总表

变化紧急度你要做的事
Compose 编译器并入 Kotlin 立刻 删掉 composeOptions,换成 kotlin.plugin.compose
强跳过默认开启 立刻 删掉大部分 @Immutable;但检查 lambda 有没有捕获会变的值
Navigation 3 立刻 新项目直接上;老项目评估迁移(下一章)。别在 Nav2 类型安全路由上停留
edge-to-edge / 大屏不锁方向 立刻 平台强制,躲不掉。补 WindowInsets,补自适应布局
核心库多平台化 该知道 不做 KMP 也要知道 —— 它解释了社区为什么在往那个方向长
Material 3 Expressive 可以先不管 视觉层,不影响架构
✚ 动手

1. 在你的项目里全局搜 @ImmutableImmutableList,数一数有多少个 —— 那就是你曾经付出的稳定性税。

2. 搜一遍 LazyColumn 里的 onClick lambda,看看它们捕获了什么。捕获了会变的值?那你就中了上面那个坑。

清单过完了。其中五条是「知道了就能立刻动手」的,但有一条不行 —— Navigation 3 值得单开一章。 因为它不是一次 API 升级,而是把「返回栈归谁管」这个问题的答案彻底掉了个个儿。下一章。