UI はローカライズ済みです。スキルの紹介と説明文書は選択した言語へ AI が自動翻訳し、コード・コマンド・パス・専門用語は原文のままです。原作者の文書が優先されます。
スキルを探す
データ・研究 · SKILL.md

validating-compose-stability

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 against it, …

Independent GitHub snapshot · Automated classification · Original documentation governs

スキルの説明

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:

  • 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

  • 1. Configure the validation block. In each Compose module's 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.

  • 2. Generate the baseline once and commit it. With the module compiled:
./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.

  • 3. Run the check in CI, after compilation. 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
  • 4. Accept an intentional stability change deliberately. When a regression is real and accepted (a justified 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.

  • 5. Exclude things that are not part of the contract. Annotate composables that should never be tracked — @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)) }
  • 6. (Optional) strict in CI, warning-only locally. Gate hard on the CI server, stay non-blocking on a developer machine:
composeStabilityAnalyzer {
    stabilityValidation { failOnStabilityChange.set(System.getenv("CI") == "true") }
}

Most CI platforms set CI=true.

Patterns

Pattern: running 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

Pattern: regenerating the baseline to "make CI green"

# 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>"

Pattern: @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) }

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

  • ./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.
  • The CI workflow's stability job has 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.

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.
保存したスキル