技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
validating-compose-stability【数据与研究】Use this skill to gate CI on Jetpack Compose stability — catch when a composable becomes unskippable/unrestartable or a parameter goes stable → unstable, before it ships and tanks recomposition. Covers the Compose Stability Analyzer Gradle plugin's `stabilityDump` (write a `.stability` baseline) and `stabilityCheck` (compare current compilation again…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: validating-compose-stability
description: >-
Use this skill to gate CI on Jetpack Compose stability — catch when a composable becomes unskippable/unrestartable or a parameter goes stable → unstable, before it ships and tanks recomposition. Covers the Compose Stability Analyzer Gradle plugin's stabilityDump (write a .stability baseline) and stabilityCheck (compare current compilation against it, fail on regression) tasks, the composeStabilityAnalyzer { stabilityValidation { … } } DSL (failOnStabilityChange, ignoreNonRegressiveChanges, ignored* lists, allowMissingBaseline, stabilityConfigurationFiles), the @IgnoreStabilityReport annotation, committing app/stability/app.stability as a team baseline, the deliberate baseline-update workflow, and GitHub Actions wiring (needs: build, since the analysis reads compiled output). Use when the user mentions "stabilityCheck", "stabilityDump", "compose stability analyzer", "stability baseline", "@IgnoreStabilityReport", "composeStabilityAnalyzer", or "fail the build on stability changes".
license: Apache-2.0. See LICENSE for complete terms.
metadata:
author: Jaewoong Eum (skydoves)
keywords:
Compose stability — whether a composable is skippable/restartable and whether its parameters are stable — is invisible until a recomposition profiler catches it in production. This skill makes it a build gate: snapshot the current stability of every composable into a committed .stability baseline, and fail CI when a change makes anything less stable. The tool is the Compose Stability Analyzer Gradle plugin. (Authoring composables for stability — @Immutable/@Stable, the compiler config file, deferred reads — is covered in the compose-performance-skills repo; this skill is purely the validation/CI layer.)
./gradlew stabilityCheck to fail CI when a composable becomes unskippable or a parameter flips stable → unstable..stability files) so changes show up in PR diffs.List parameter or a non-@Immutable data class added to a hot composable.stabilityDump, stabilityCheck, composeStabilityAnalyzer, @IgnoreStabilityReport, or "stability baseline".@Immutable/@Stable, kotlinx.collections.immutable, stability_config.conf for external types, deferred state reads, derivedStateOf) — that is Compose performance authoring; see the compose-performance-skills repo's stability skills. This skill only validates and gates.Recomposer metrics, composition tracing) — different tool, runtime not build time.../../setup/choosing-test-rule-vs-runtest/SKILL.md and ../../preview/capturing-preview-screenshots-in-ci/SKILL.md.stabilityCheck/stabilityDump. In CI this means a needs: build (or running :module:compileDebugKotlin first) — running the check before compilation produces wrong results or fails.<module>/stability/<module>.stability (generated by stabilityDump).build.gradle.kts:composeStabilityAnalyzer {
stabilityValidation {
enabled.set(true)
outputDir.set(layout.projectDirectory.dir("stability")) // where the .stability baseline lives
includeTests.set(false)
ignoredPackages.set(listOf("com.example.internal"))
ignoredClasses.set(listOf("PreviewComposables"))
ignoredProjects.set(listOf("benchmarks", "examples"))
failOnStabilityChange.set(true) // build fails on stability changes; false => warning-only
ignoreNonRegressiveChanges.set(false) // true => only regressions count; new-stable / improvements ignored
allowMissingBaseline.set(false) // true => no baseline yet is not a failure (bootstrap only)
stabilityConfigurationFiles.add( // same format as the Compose compiler's stability config
rootProject.layout.projectDirectory.file("stability_config.conf")
)
}
}
Key knobs: failOnStabilityChange (the gate), ignoreNonRegressiveChanges (report only regressions, ignore improvements and newly-added stable composables), stabilityConfigurationFiles (the Compose-compiler-format file declaring external types stable), and the ignored* lists for packages/classes/modules outside the contract.
./gradlew :app:compileDebugKotlin
./gradlew :app:stabilityDump # writes app/stability/app.stability
git add app/stability/app.stability
git commit -m "Add Compose stability baseline"
The .stability file is human-readable: per composable it lists the fully-qualified signature, skippable/restartable status, and each parameter's stability classification with the reason. Android projects get variant-specific tasks (debugStabilityDump, releaseStabilityCheck, …); multi-module projects get one .stability file per module — ./gradlew stabilityDump runs them all, or target a module.
stabilityCheck compares the current compilation against the committed baseline and reports three change kinds: ~ stability regression (a composable/parameter became less stable), + a composable added, - a composable removed. On a regression the build fails with a message naming the affected composables and how their stability changed, and tells you to run stabilityDump if the change was intentional.# .github/workflows/ci.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '21', distribution: 'zulu' }
- run: ./gradlew :app:compileDebugKotlin
stability_check:
name: Compose Stability Check
runs-on: ubuntu-latest
needs: build # MANDATORY — the analysis reads compiled output
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '21', distribution: 'zulu' }
- run: ./gradlew stabilityCheck
List parameter, an external type that genuinely can't be stable), update the baseline as a documented commit — never silently:./gradlew :app:compileDebugKotlin
./gradlew :app:stabilityDump
git add app/stability/app.stability
git commit -m "Update stability baseline: PokemonList now takes List<Pokemon> — justified by [reason]"
The diff in the .stability file is the review artifact; the commit message is the audit trail.
@Preview functions, internal scaffolding — with @IgnoreStabilityReport; they drop out of both the dump and the check. For whole packages/classes/modules, use ignoredPackages/ignoredClasses/ignoredProjects.@IgnoreStabilityReport
@Preview
@Composable
fun UserCardPreview() { UserCard(user = User("John", 30)) }
composeStabilityAnalyzer {
stabilityValidation { failOnStabilityChange.set(System.getenv("CI") == "true") }
}
Most CI platforms set CI=true.
stabilityCheck before compilation# WRONG
stability_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./gradlew stabilityCheck # no compiled output yet
# WRONG because: the analyzer reads the Compose compiler's output. With nothing compiled it
# either fails or reports incorrect results. The check must depend on a build step.
# RIGHT
stability_check:
runs-on: ubuntu-latest
needs: build # or: - run: ./gradlew :app:compileDebugKotlin stabilityCheck
steps:
- uses: actions/checkout@v4
- run: ./gradlew stabilityCheck
# WRONG — CI failed on a stability regression, so:
./gradlew stabilityDump && git commit -am "fix stability check"
# WRONG because: stabilityDump rewrites the baseline to whatever the code currently produces, so
# the check trivially passes — the regression ships, undocumented. The dump is for *accepting* a
# change you understand, with a commit message that says why; it is not a way to silence the gate.
# RIGHT — investigate the ~ entries first; only then, if the change is justified:
./gradlew :app:compileDebugKotlin :app:stabilityDump
git add app/stability/app.stability
git commit -m "Update stability baseline: <composable> <what changed> — justified by <reason>"
@Preview functions polluting the baseline// WRONG — preview functions in the .stability baseline, churning it on every preview edit
@Preview @Composable fun ProfilePreview() { Profile(sampleUser) }
// WRONG because: previews are tooling entry points, not API. Tracking them means the baseline
// diffs every time someone tweaks a preview, drowning real regressions.
// RIGHT — annotate them out (or ignoredClasses = listOf("...Preview...") for a naming convention)
@IgnoreStabilityReport @Preview @Composable fun ProfilePreview() { Profile(sampleUser) }
.stability baseline files to version control — they are the shared contract; an uncommitted baseline gates nothing.stabilityCheck only after Kotlin compilation (CI: needs: a build job, or compile in the same Gradle invocation). The analyzer consumes compiled output.stabilityDump baseline update as a deliberate, reviewed commit with a justification in the message — never as a way to clear a failing check.@Preview and other non-API composables from the contract via @IgnoreStabilityReport (or ignoredClasses/ignoredPackages) so the baseline diffs only on real changes.failOnStabilityChange = false (or allowMissingBaseline = true) permanently — those are for bootstrapping; once a baseline exists, the gate must fail on regressions.@Immutable/@Stable, immutable collections, the compiler stability config); see the compose-performance-skills repo.ignoreNonRegressiveChanges = true if the team only cares about regressions and finds +/new-stable noise distracting.failOnStabilityChange.set(System.getenv("CI") == "true")../gradlew stabilityDump produces <module>/stability/<module>.stability and it is committed (git status clean after a no-op dump)../gradlew stabilityCheck passes on the committed baseline and fails when a composable is deliberately made unstable (e.g. add a List<T> parameter to a tracked composable) with a message naming it and a ~ entry.needs: build (or compiles before stabilityCheck).@Preview / scaffolding composables carry @IgnoreStabilityReport (or match an ignoredClasses pattern) and do not appear in the .stability file.failOnStabilityChange is true (or CI-gated to true) and allowMissingBaseline is false once the baseline exists.stabilityDump / stabilityCheck tasks, the composeStabilityAnalyzer { stabilityValidation { … } } DSL and every option, .stability baseline format, ~/+/- change types, @IgnoreStabilityReport, multi-module behavior.needs: build dependency, build-failure behavior, the baseline-update workflow, failOnStabilityChange.set(System.getenv("CI") == "true").@Immutable/@Stable, immutable collections, the compiler stability_configuration_path file) — the authoring side this gate protects.../../preview/capturing-preview-screenshots-in-ci/SKILL.md — another Compose CI gate (device-rendered preview catalog); same needs: build-style ordering concerns.