mcpField
S
stabilizing-compose-types

stabilizing-compose-types

github.com/skydoves开发520133 次安装19 周前更新
代码仓库

关于 stabilizing-compose-types

面向 Android Compose 开发者的类型稳定化技能,用于处理被标记为不稳定的类型。它按三层顺序指导操作:重构类型、在契约成立时添加注解,最后为第三方或 Java 类型配置稳定性。

主要功能

  1. 01采用三层稳定化策略
  2. 02优先使用不可变属性和集合
  3. 03恰当选择 @Immutable 或 @Stable
  4. 04为外部类型配置稳定性
  5. 05在可组合项外收集 Flow

适用场景

  1. 01处理编译器报告中的不稳定参数
  2. 02稳定领域模型和集合参数
  3. 03处理第三方或 Java 类型
  4. 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+ 才支持 stabilityConfigurationFiles DSL(复数形式;旧的单数属性 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 或不稳定嵌套类型的类型上。

参考资料

评论

支持 Markdown