关于 stabilizing-compose-types
面向 Android Compose 开发者的类型稳定化技能,用于处理被标记为不稳定的类型。它按三层顺序指导操作:重构类型、在契约成立时添加注解,最后为第三方或 Java 类型配置稳定性。
主要功能
- 01采用三层稳定化策略
- 02优先使用不可变属性和集合
- 03恰当选择 @Immutable 或 @Stable
- 04为外部类型配置稳定性
- 05在可组合项外收集 Flow
适用场景
- 01处理编译器报告中的不稳定参数
- 02稳定领域模型和集合参数
- 03处理第三方或 Java 类型
- 04将 Flow 收集移出可组合项
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ skydoves/ compose-performance-skills --skill stabilizing-compose-types安装后在 Claude Code 中输入/stabilizing-compose-types确认已加载。
附带文件references/stability-config-syntax.md
详细介绍
稳定化 Compose 类型——按顺序分三个层级
诊断步骤(见 ../diagnosing-compose-stability/SKILL.md)确定不稳定类型后,本技能将引导 Claude 修复这些类型。采用严格的三级递进策略:(1)通过结构重写(val + 不可变字段)使类型真正稳定——无需注解;(2)当源码由你维护且遵守约定时,使用 @Immutable 或 @Stable 注解;(3)对第三方或 Java 类型使用 stabilityConfigurationFile。切勿颠倒顺序——注解是一种约定,不是魔法。
何时使用本技能
- 编译器报告(composables.txt / classes.txt)显示一个或多个
unstable参数。 - 开发者询问如何稳定化领域类、
List<Foo>参数、java.time.LocalDateTime或第三方类型。 - 开发者提到
@Immutable、@Stable、compose-stable-marker、compose-runtime-annotation、kotlinx.collections.immutable、stabilityConfigurationFiles或stability_config.conf。 @TraceRecomposition日志显示,由于某个参数在每次重组时都会重新分配,导致发生重组。
何时不使用本技能
- 尚未确定不稳定类型——请先运行
../diagnosing-compose-stability/SKILL.md。 - 症状是状态在错误阶段读取(
Modifier.alpha(state.value));请使用../../recomposition/deferring-state-reads/SKILL.md。 - 症状是
derivedStateOf问题;请使用../../recomposition/choosing-derivedstateof/SKILL.md。 - 原因是
Flow<T>参数;解决方法是在上游收集——请参阅../../side-effects/collecting-flows-safely/SKILL.md,不要将Flow注解为稳定类型。
先决条件
- 已运行
../diagnosing-compose-stability/SKILL.md;开发者已获得一份明确的不稳定类型列表。 - 使用 Kotlin 2.0.0+,并已应用
org.jetbrains.kotlin.plugin.compose。默认启用 Strong Skipping。 - 对于不含
compose-runtime的纯 Kotlin / data 模块:需在类路径中添加androidx.compose.runtime:runtime-annotation(官方)或com.github.skydoves:compose-stable-marker(旧版)。两者都提供@Stable/@Immutable/@StableMarker,且不会引入完整的 runtime。 - 对于第 3 层修复:Compose Compiler 1.5.5+ 才支持
stabilityConfigurationFilesDSL(复数形式;旧的单数属性stabilityConfigurationFile已弃用,见下方脚注)。
工作流 — 决策树
-
1. 我们是否拥有不稳定类型的源代码? 如果是,使用第 1 或第 2 层。如果不是(Java 标准库、第三方 SDK),则跳到第 3 层。
-
2. 第 1 层 — 重构为真正稳定的类型。 能否将每个属性都设为类型已经稳定的
val?如果可以,必须先进行此更改。无需注解,也不会隐含任何契约。编译器会自动推断稳定性。
// WRONG
data class Snack(
var name: String,
val tags: Set<String>,
)
// WRONG because: `var name` is an observable that does not notify Compose, and `Set<String>` is the standard library interface so its stability is Unknown — the data class is unstable on two axes.
// RIGHT
@Immutable
data class Snack(
val name: String,
val tags: ImmutableSet<String>,
)
-
3. 不稳定属性是否为集合? 将
kotlin.collections.List、Set、Map替换为kotlinx.collections.immutable.ImmutableList、ImmutableSet、ImmutableMap。使用persistentListOf()、persistentSetOf()、persistentMapOf()工厂函数构建,或使用.toImmutableList()适配器。kotlinx-collections-immutable 工件提供了 Compose Compiler 能识别的已知稳定位掩码0b1。**首选:**优先采用此方案,而不是在stability_config.conf中将kotlin.collections.*加入白名单,因为类型系统会强制保证不可变性。 -
4. 源代码是否属于纯 Kotlin/数据模块,且不依赖
compose-runtime? 使用androidx.compose.runtime:runtime-annotation(官方推荐)或com.github.skydoves:compose-stable-marker(旧版)。两者都允许数据模块为类型添加注解,而无需引入完整的 Compose 运行时。Skydoves 的热门观点 #6:纯 Kotlin/数据模块可以使用 compose-stable-marker(或更新的官方 compose-runtime-annotation),而无需引入完整的 compose-runtime。
// shared data module — build.gradle.kts
dependencies {
// Official, preferred:
compileOnly("androidx.compose.runtime:runtime-annotation:<version>")
// Or legacy skydoves:
// implementation("com.github.skydoves:compose-stable-marker:<version>")
}
- 5. 第 2 层 — 选择
@Immutable或@Stable。 如果每个属性都是val,并且每个嵌套值本身都不可变,且equals()采用结构比较,则使用@Immutable。否则,对于值可能变化、但其变更会被 Compose 观察到的类型(例如包含MutableState的持有者),使用@Stable。
编译器对 @Immutable 的处理比对 @Stable 更积极。对于 @Immutable 类型,如果调用点的每个构造函数参数都是编译时常量(例如字面数字、稳定类型的顶层 val,或参数均为常量的其他 @Immutable 类型),编译器会执行静态表达式提升:将构造的实例提升为单例,对 lambda 捕获点去重,并在 composables.txt 中将 @Immutable 参数标记为 @static。大多数文章都称 @Stable 和 @Immutable 可以互换使用——事实并非如此。
// RIGHT — @Immutable: every property val, every property type immutable
@Immutable
data class ThemeColors(
val primary: Color,
val onPrimary: Color,
val background: Color,
)
// RIGHT — @Stable: mutable but mutations notify Compose via Snapshot
@Stable
class CartState {
var total: Money by mutableStateOf(Money.ZERO)
val lines: SnapshotStateList<Line> = mutableStateListOf()
}
- 6. 第 3 层 — 第三方或 Java 类型。 使用
stabilityConfigurationFiles(复数)。在项目根目录创建stability_config.conf,列出精确类名或通配符模式;通过stabilityConfigurationFiles.add(...)将其接入composeCompiler { }代码块。完整语法见references/stability-config-syntax.md。
// build.gradle.kts (root or composable module)
composeCompiler {
stabilityConfigurationFiles.add(
rootProject.layout.projectDirectory.file("stability_config.conf")
)
}
脚注 — 旧版写法。 较旧的项目可能仍通过单数属性
stabilityConfigurationFile = ...接入该文件。在现代 Compose Compiler Gradle 插件版本中,此属性带有@Deprecated("Use the stabilityConfigurationFiles option instead")标记——请优先使用上文所示的复数形式stabilityConfigurationFiles.add(...)。
# stability_config.conf — opt-in contract
java.time.LocalDateTime
java.time.LocalDate
kotlin.collections.List
kotlin.collections.Set
kotlin.collections.Map
com.example.thirdparty.**
- 7. 第 1 至第 3 层都不适用? 将其封装在
@Stable class StableHolder<T>(val item: T)中。这是兜底方案——持有者的身份稳定,相等性由item决定,Compose 可以跳过它。
@Stable
class StableHolder<T>(val item: T) {
override fun equals(other: Any?) = other is StableHolder<*> && other.item == item
override fun hashCode() = item?.hashCode() ?: 0
}
- 8.
Flow<T>参数——不要将其注解为稳定类型。 Skydoves 的热门观点 #4:Flow参数是不稳定的。不要将流传递给可组合项;应在 ViewModel 中收集,或使用collectAsStateWithLifecycle。
// WRONG
@Composable
fun Feed(items: Flow<List<Item>>) { /* ... */ }
// WRONG because: Flow has no observable identity; the composable cannot skip on it, and a downstream `collectAsState` here detaches from lifecycle.
// RIGHT — collect upstream and pass the resolved value
@Composable
fun FeedRoute(viewModel: FeedViewModel = viewModel()) {
val items by viewModel.items.collectAsStateWithLifecycle()
Feed(items = items)
}
@Composable
fun Feed(items: ImmutableList<Item>) { /* ... */ }
- 9. 重新运行诊断技能,确认之前不稳定的参数现在已变为
stable或runtime。
模式
模式:带有 var 和 Set 字段的数据类
// WRONG
data class Snack(
var name: String,
val tags: Set<String>,
)
// WRONG because: `var` blocks compile-time stability inference; `Set<String>` is an interface with unknown implementations.
// RIGHT
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
@Immutable
data class Snack(
val name: String,
val tags: ImmutableSet<String> = persistentSetOf(),
)
模式:可组合项上的 List<Item> 参数
// WRONG
@Composable
fun ItemList(items: List<Item>) { /* ... */ }
// WRONG because: `kotlin.collections.List` is an interface; the compiler cannot prove every implementation is immutable, so the parameter is `unstable` and skipping is disabled (or pinned to `===` under Strong Skipping, which still fails on every fresh allocation).
// RIGHT
import kotlinx.collections.immutable.ImmutableList
@Composable
fun ItemList(items: ImmutableList<Item>) { /* ... */ }
生产端:
val items: ImmutableList<Item> = repository.items().toImmutableList()
模式:java.time.LocalDateTime 被标记为不稳定
Java 时间类型是单独编译的,且没有注解,因此推断会落入“未知”。Tier 3 才是正确答案。
# stability_config.conf
java.time.LocalDateTime
java.time.LocalDate
java.time.Instant
java.time.ZonedDateTime
java.time.Duration
// WRONG — annotate a wrapper to "force" stability without honoring the contract
@Stable
class DateWrapper(var date: LocalDateTime)
// WRONG because: `var` field, no Snapshot notification — the @Stable contract is broken; recompositions will be silently missed.
// RIGHT — whitelist the immutable JDK type via stability config
// stability_config.conf: java.time.LocalDateTime
@Immutable
data class Order(val placedAt: LocalDateTime, val total: Money)
模式:可组合项上的 Flow<T> 参数
// WRONG
@Composable
fun Detail(productFlow: Flow<Product>) {
val product by productFlow.collectAsState(initial = null)
/* ... */
}
// WRONG because: `Flow` is a cold producer with no observable identity; the composable can never skip on it, and `collectAsState` (no `WithLifecycle`) keeps collecting in the background.
// RIGHT — hoist collection to the route
@Composable
fun DetailRoute(viewModel: DetailViewModel = viewModel()) {
val product by viewModel.product.collectAsStateWithLifecycle()
Detail(product = product)
}
@Composable
fun Detail(product: Product?) { /* ... */ }
交叉引用:../../side-effects/collecting-flows-safely/SKILL.md。
模式:将内联可组合项包装起来作为“修复”
// WRONG — wrapping a Row in an extracted composable to "force" skippability
@Composable
fun ItemRow(item: Item) {
Row { /* ... */ }
}
// WRONG because: `Row`/`Column`/`Box` are inline composables — they are NOT restartable/skippable to begin with (skydoves hot take #3). Wrapping them creates a new restart scope, which can change behavior unpredictably without addressing the root unstable parameter.
// RIGHT — make the parameter stable, leave the inline composable alone
@Immutable
data class Item(val id: Long, val name: String)
@Composable
fun ItemRow(item: Item) {
Row { /* ... */ }
}
模式:@Immutable 与 @Stable——选择更强的一种
// SUFFICIENT but suboptimal
@Stable
data class ThemeColors(
val primary: Color,
val onPrimary: Color,
)
// Sufficient because: the compiler still treats it as stable.
// Suboptimal because: @Immutable would additionally enable static expression promotion when ThemeColors is constructed with all-constant args.
// PREFERRED
@Immutable
data class ThemeColors(
val primary: Color,
val onPrimary: Color,
)
在 composables.txt 中,像 ThemeColors(Color(0xFFFFFFFF), Color(0xFF000000)) 这样的调用随后会报告为 stable @static themeColors: ThemeColors——构造的实例会被提升为单例。MUST 在类型符合条件时优先使用 @Immutable。
强制规则
- MUST NOT 在不遵守契约的情况下将类型标注为
@Stable或@Immutable。Skydoves 的犀利观点 #2:稳定性配置是一份契约,不是魔法咒语——违反契约会导致重组被悄然遗漏。 - MUST 在每个属性都是现有不可变类型的
val时,优先使用@Immutable而非@Stable。对于@Immutable,编译器会执行更强的优化(静态表达式提升、lambda 单例、编译期默认值求值)。 - MUST 优先使用
kotlinx.collections.immutable,而不是将可变集合标注为稳定。编译器为这些类型内置了已知稳定的位掩码。 - MUST NOT 包装内联可组合项(
Row/Column/Box)来“强制”实现可跳过性。内联可组合项本身不可重启;包装会改变作用域,却无法解决根本原因。 - MUST NOT 将
Flow<T>作为可组合项参数传入。使用collectAsStateWithLifecycle收集上游数据。 - MUST 应用修复后重新运行
../diagnosing-compose-stability/SKILL.md;先前不稳定的参数现在MUST报告为stable或runtime。 - **PREFERRED:**使用
stabilityConfigurationFiles(复数形式;stabilityConfigurationFiles.add(...)),而不是在团队不负责的多个模块中零散添加注解。 - **PREFERRED:**在纯 Kotlin / 数据模块中,使用
androidx.compose.runtime:runtime-annotation(官方)或com.github.skydoves:compose-stable-marker(旧版),这样无需完整的compose-runtime依赖也能使用该注解。
验证
- 在启用
composeCompiler { reportsDestination = ... }的情况下,重新运行发布模式构建。 - 在
composables.txt中,先前所有标记为unstable的参数现在都为stable或runtime。 - 在
classes.txt中,所有已修复的类现在都为stable class …或runtime stable class …——目标类型不再有任何unstable标记。 - 如果使用了
stabilityConfigurationFiles,请确认每个条目都遵守契约(不存在隐藏的可变性、结构化equals,也没有绕过 Snapshot 的可观察对象)。 - 对先前频繁重组的可组合项使用
@TraceRecomposition(skydoves/compose-stability-analyzer)后,状态每次变化时显示的重组次数都会降至预期值。 - 没有将
@Stable/@Immutable注解添加到仍包含var或不稳定嵌套类型的类型上。
参考资料
- Android Developers — 修复稳定性问题:https://developer.android.com/develop/ui/compose/performance/stability/fix
- Android Developers — 稳定性概览:https://developer.android.com/develop/ui/compose/performance/stability
- Android Developers — 强跳过:https://developer.android.com/develop/ui/compose/performance/stability/strongskipping
- Ben Trengrove — 稳定性详解:https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8
- Ben Trengrove — 稳定性优化的新方法:https://medium.com/androiddevelopers/new-ways-of-optimizing-stability-in-jetpack-compose-038106c283cc
- Ben Trengrove — 强跳过模式详解:https://medium.com/androiddevelopers/jetpack-compose-strong-skipping-mode-explained-cbdb2aa4b900
- Manuel Vivo — 安全地使用 Flow:https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3
- skydoves — 掌握稳定性,优化应用性能:https://medium.com/proandroiddev/optimize-app-performance-by-mastering-stability-in-jetpack-compose-69f40a8c785d
- skydoves — 6 条 Jetpack Compose 指南:https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
- skydoves — compose-stable-marker:https://github.com/skydoves/compose-stable-marker
- kotlinx.collections.immutable:https://github.com/Kotlin/kotlinx.collections.immutable
references/stability-config-syntax.md—stability_config.conf的完整语法及示例。