| 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":["jetpack-compose","compose-stability","recomposition","stability-analyzer","stabilityCheck","stabilityDump","stability-baseline","gradle-plugin","ci-cd","github-actions"]} |
Validating Compose Stability — A CI Gate On Skippability And Parameter Stability
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.)
When to use this skill
- The user wants
./gradlew stabilityCheck to fail CI when a composable becomes unskippable or a parameter flips stable → unstable.
- The user wants a committed, reviewable baseline of every composable's stability (
.stability files) so changes show up in PR diffs.
- A recomposition regression shipped because nothing flagged a
List parameter or a non-@Immutable data class added to a hot composable.
- The user mentions
stabilityDump, stabilityCheck, composeStabilityAnalyzer, @IgnoreStabilityReport, or "stability baseline".
- The user wants warning-only locally and hard-fail in CI.
When NOT to use this skill
- The user wants to fix an unstable parameter (
@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.
- The user wants to find which recompositions are firing at runtime (Layout Inspector recomposition counts,
Recomposer metrics, composition tracing) — different tool, runtime not build time.
- The user is doing UI behavioral or screenshot testing — see
../../setup/choosing-test-rule-vs-runtest/SKILL.md and ../../preview/capturing-preview-screenshots-in-ci/SKILL.md.
Prerequisites
- The Compose Stability Analyzer Gradle plugin applied to each Compose module (apply via the plugins block; coordinates and version from the project's catalog / the plugin docs).
- Compose compiler metrics available — the plugin analyzes compiled output, so the module's Kotlin compilation must run before
stabilityCheck/stabilityDump. In CI this means a needs: build (or running :module:compileDebugKotlin first) — running the check before compilation produces wrong results or fails.
- A baseline committed to version control:
<module>/stability/<module>.stability (generated by stabilityDump).
Workflow
composeStabilityAnalyzer {
stabilityValidation {
enabled.set(true)
outputDir.set(layout.projectDirectory.dir("stability"))
includeTests.set(false)
ignoredPackages.set(listOf("com.example.internal"))
ignoredClasses.set(listOf("PreviewComposables"))
ignoredProjects.set(listOf("benchmarks", "examples"))
failOnStabilityChange.set(true)
ignoreNonRegressiveChanges.set(false)
allowMissingBaseline.set(false)
stabilityConfigurationFiles.add(
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
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.
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
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '21', distribution: 'zulu' }
- run: ./gradlew stabilityCheck
./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.
@IgnoreStabilityReport
@Preview
@Composable
fun UserCardPreview() { UserCard(user = User("John", 30)) }
composeStabilityAnalyzer {
stabilityValidation { failOnStabilityChange.set(System.getenv("CI") == "true") }
}
Most CI platforms set CI=true.
Patterns
Pattern: running stabilityCheck before compilation
stability_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./gradlew stabilityCheck
stability_check:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: ./gradlew stabilityCheck
Pattern: regenerating the baseline to "make CI green"
./gradlew stabilityDump && git commit -am "fix stability check"
./gradlew :app:compileDebugKotlin :app:stabilityDump
git add app/stability/app.stability
git commit -m "Update stability baseline: <composable> <what changed> — justified by <reason>"
Pattern: @Preview functions polluting the baseline
@Preview @Composable fun ProfilePreview() { Profile(sampleUser) }
@IgnoreStabilityReport @Preview @Composable fun ProfilePreview() { Profile(sampleUser) }
Mandatory rules
- MUST commit the generated
.stability baseline files to version control — they are the shared contract; an uncommitted baseline gates nothing.
- MUST run
stabilityCheck only after Kotlin compilation (CI: needs: a build job, or compile in the same Gradle invocation). The analyzer consumes compiled output.
- MUST treat a
stabilityDump baseline update as a deliberate, reviewed commit with a justification in the message — never as a way to clear a failing check.
- MUST exclude
@Preview and other non-API composables from the contract via @IgnoreStabilityReport (or ignoredClasses/ignoredPackages) so the baseline diffs only on real changes.
- MUST NOT set
failOnStabilityChange = false (or allowMissingBaseline = true) permanently — those are for bootstrapping; once a baseline exists, the gate must fail on regressions.
- MUST NOT use this skill as the place to fix instability — that is Compose authoring (
@Immutable/@Stable, immutable collections, the compiler stability config); see the compose-performance-skills repo.
- PREFERRED:
ignoreNonRegressiveChanges = true if the team only cares about regressions and finds +/new-stable noise distracting.
- PREFERRED: strict in CI, warning-only locally via
failOnStabilityChange.set(System.getenv("CI") == "true").
Verification
References
- skydoves.github.io/compose-stability-analyzer/gradle-plugin/stability-validation/ —
stabilityDump / stabilityCheck tasks, the composeStabilityAnalyzer { stabilityValidation { … } } DSL and every option, .stability baseline format, ~/+/- change types, @IgnoreStabilityReport, multi-module behavior.
- skydoves.github.io/compose-stability-analyzer/gradle-plugin/ci-cd/ — GitHub Actions wiring, the mandatory
needs: build dependency, build-failure behavior, the baseline-update workflow, failOnStabilityChange.set(System.getenv("CI") == "true").
- github.com/skydoves/compose-stability-analyzer — the plugin source, coordinates, and full configuration reference.
- developer.android.com/develop/ui/compose/performance/stability — Compose stability concepts (skippable/restartable, stable parameters) the baseline is built on.
- developer.android.com/develop/ui/compose/performance/stability/fix — fixing instability (
@Immutable/@Stable, immutable collections, the compiler stability_configuration_path file) — the authoring side this gate protects.
- Sibling skill:
../../preview/capturing-preview-screenshots-in-ci/SKILL.md — another Compose CI gate (device-rendered preview catalog); same needs: build-style ordering concerns.