mcpField
C
compose-stability-diagnostics

compose-stability-diagnostics

github.com/chrisbanes开发1.1k995 次安装1 天前更新
代码仓库

关于 compose-stability-diagnostics

面向 Jetpack Compose 开发者的诊断指南,帮助依据编译器报告和运行时证据减少不必要的重组。它区分稳定性语义、实例变化与调用点输入导致的问题。

主要功能

  1. 01理解强跳过的比较规则
  2. 02生成并检查编译器报告
  3. 03诊断不稳定参数与实例变化
  4. 04稳定延迟列表项输入
  5. 05通过重复测量验证修复

适用场景

  1. 01调查界面交互期间的重组
  2. 02检查可组合项未跳过的原因
  3. 03减少延迟列表项 Lambda 引发的重组
  4. 04发布前验证稳定性调整

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/chrisbanes/skills --skill compose-stability-diagnostics

安装后在 Claude Code 中输入/compose-stability-diagnostics确认已加载。

详细介绍

Compose 稳定性诊断

核心原则

Compose 参数修复应以证据为起点。先确定编译器模式和参数比较行为,再修改真正导致无法跳过重组的模型或调用点。

Kotlin 2.0.20+ 默认启用强跳过。不稳定参数不再自动导致可重启的可组合项无法跳过,但不稳定参数会按实例身份(===)比较,而稳定参数会按相等性(equals)比较。频繁变化的不稳定实例仍可能导致无法跳过重组。

诊断流程

  1. 确认症状:重组次数、编译器报告输出,或疑似频繁变化的参数。
  2. 确定编译器模式:Kotlin/Compose Compiler 版本,以及是否启用了强跳过。
  3. 生成或查看已发布变体的 Compose Compiler 报告。
  4. 对每个可疑参数,判断问题出在稳定性语义、实例频繁变化,还是调用方创建了 lambda/派生值。
  5. 应用最轻量的修复,使类型或调用点如实反映其行为。
  6. 在相同交互中重新测量,或重新查看同一份报告,然后再声称问题已修复。

1. 首先理解强跳过

在 Kotlin 2.0.20+ 中,强跳过默认启用。在这种模式下:

  • 即使参数不稳定,可重启的可组合项仍可跳过重组,除非显式选择不启用。
  • 稳定参数使用 equals 比较。
  • 不稳定参数使用实例相等性(===)比较。
  • 可组合项内的 lambda 会根据捕获内容自动记忆化。

请问:“这些参数的比较方式是否符合预期?调用方是否每帧都会创建新的不稳定实例?”

对于较旧的编译器配置或禁用了强跳过的情况,旧规则仍然适用:带有不稳定参数的可重启可组合项可以重启,但无法跳过重组。

2. 生成编译器报告

使用 Kotlin 2.0+ 时,Compose Compiler 通过 Kotlin Gradle 插件配置:

plugins {
    alias(libs.plugins.android.application) // or android.library / jvm
    alias(libs.plugins.kotlin.android)      // or kotlin.multiplatform / kotlin.jvm
    alias(libs.plugins.compose.compiler)
}

if (providers.gradleProperty("composeReports").orNull == "true") {
    composeCompiler {
        reportsDestination = layout.buildDirectory.dir("compose_compiler")
        metricsDestination = layout.buildDirectory.dir("compose_compiler")
    }
}

然后构建你关心其编译器配置的变体,例如:

./gradlew :app:assembleRelease -PcomposeReports=true

运行时性能分析请使用 release/非调试构建。编译器报告是构建时输出,因此关键是确保使用与你发布时相同的变体和编译器标志。

关键文件:

文件用途
<module>-classes.txt类和属性的稳定性
<module>-composables.txt可重启/可跳过状态和参数稳定性
<module>-composables.csv相同数据的可排序形式
<module>-module.json汇总指标

3. 只修复已证实的参数问题

选择能如实反映类型不可变性或相等性语义的最轻量修复。

不可变集合

如果报告显示 UI 状态使用了集合接口,优先在 UI 状态边界使用 kotlinx.collections.immutable:

// Before: unstable collection interfaces
data class UiState(val items: List<Item>, val tags: Set<String>)

// After: immutable collection contracts
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet

data class UiState(val items: ImmutableList<Item>, val tags: ImmutableSet<String>)

生产者在边界处调用 .toImmutableList()/.toImmutableSet() 转换一次。

@Immutable/@Stable
  • 当每个属性实际上都不可变,且相等性涵盖所有可观察状态时,使用 @Immutable。
  • 对于其可变状态可被 Compose 观察到的类型,通常通过 MutableState 实现,使用 @Stable。

不要为了让报告消失而添加注解。错误的稳定性承诺可能导致 UI 显示过时内容。

第三方不可变类型

对于无法添加注解、但可以如实视为不可变的类型,请使用 stabilityConfigurationFiles:

composeCompiler {
    stabilityConfigurationFiles.add(
        rootProject.layout.projectDirectory.file("compose_stability.conf"),
    )
}
java.math.BigDecimal
java.math.BigInteger
java.time.*
kotlinx.datetime.*

只列出你愿意承诺为不可变的类型。不要列出 java.util.Date 等可变类型。

4. 稳定惰性项输入

如果惰性项重组是由调用点频繁变化引起的,应稳定传递给每个项的值,而不是盲目地给模型添加注解。

将每项生命周期内保持稳定的输入提升到外部并记住:

// ❌ BAD — new lambda instances when parent recomposes
items(list, key = { it.id }) { item ->
    RowCard(
        onClick = { onItemClick(item.id) },
        isHighlighted = { item.id == selectedId },
    )
}

// ✅ GOOD — stable captures for this item instance
items(list, key = { it.id }) { item ->
    val onClick = remember(item.id) { { onItemClick(item.id) } }
    val isHighlighted = remember(item.id, selectedId) { item.id == selectedId }
    RowCard(onClick = onClick, isHighlighted = isHighlighted)
}

如果行位置信息(isFirst、isLast、圆角半径)仅取决于索引,也应使用 remember(index) { … } 将其提升到外部。但不要指望仅靠这一点就能修复回写或跨行测量问题。

提升后,通过重组次数断言验证焦点移动和插入行为。

快速参考

症状诊断修复方法
使用 Kotlin 2.0.20+,但旧文档说不稳定就意味着不可跳过强跳过改变了默认行为改为检查比较语义和实例是否频繁变化
unstable val items: List<Item>接口集合使用 ImmutableList<Item> 或其他真正不可变的封装类型
unstable val price: BigDecimal外部不可变类型将其添加到稳定性配置中
对含有可变内部状态的类型使用了 @Immutable做出了不实承诺修复模型或移除注解
尽管启用了强跳过,可组合项仍无法有效跳过每次重组都会创建新的不稳定实例使用 remember、提升到外部,或让类型稳定/基于相等性进行比较
即使数据未变化,父项重组时惰性项也会重组父项每次重组都会创建新的 Lambda 或派生值实例(§4)使用 remember(item.id) { … } 将每项的值提升到外部
未生成报告缺少 Compose 编译器插件或未设置标志应用 org.jetbrains.kotlin.plugin.compose 并启用报告目标

何时不应应用

  • 问题是跨阶段回写或跨行测量读取。请使用 compose-state-deferred-reads。
  • 问题是在组合阶段读取频繁变化的 State,例如滚动或动画。请使用 compose-state-deferred-reads。
  • 重组次数与实际数据变化相符。
  • Bug 是数据错误或状态过期,而不是工作量过多。
  • 代码仅用于测试,可读性比报告整洁更重要。

相关内容

评论

支持 Markdown