Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use this skill to diagnose Jetpack Compose stability problems by enabling and reading the Compose Compiler Reports (classes.txt, composables.txt, composables.csv, module.json). Covers the Gradle DSL, the release-only build requirement, and how to interpret per-class and per-composable stability annotations including stable, unstable, runtime, restartable, skippable, readonly, @static, and @dynamic markers. Use when the developer asks "why does this recompose", reports jank, dropped frames, slow
.claude/skills/skydoves-diagnosing-compose-stability/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 47% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 151% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 166% | 0% |
Compose skips recomposition by comparing parameters. When a parameter is unstable, skipping is disabled — this skill tells Claude how to find out which parameters are unstable and why. The output is a prioritized list of unstable types and non-skippable composables; the fix lives in ../stabilizing-compose-types/SKILL.md.
@TraceRecomposition log shows recomposition counts that exceed the number of meaningful state changes.classes.txt, composables.txt, composables.csv, module.json, or "non-skippable".List<Foo>, LocalDateTime, or a domain type is stable.../stabilizing-compose-types/SKILL.md.Modifier.alpha(state.value)); use ../../recomposition/deferring-state-reads/SKILL.md.derivedStateOf misuse; use ../../recomposition/choosing-derivedstateof/SKILL.md.../enforcing-stability-in-ci/SKILL.md.id("org.jetbrains.kotlin.plugin.compose"). The pre-2.0 kotlinCompilerExtensionVersion flow is obsolete.org.jetbrains.kotlin.plugin.compose Gradle plugin (Kotlin 2.0+). The composeCompiler { … } extension is owned by that plugin; AGP version is incidental.build.gradle.kts. Scope to release only — debug builds emit misleading data.kotlin// app/build.gradle.kts (or any compose module) plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.plugin.compose") } composeCompiler { // Only emit reports for release to avoid Live Literals noise. val isReleaseBuild = providers.gradleProperty("composeCompilerReports").orNull == "true" if (isReleaseBuild) { reportsDestination = layout.buildDirectory.dir("compose_compiler") metricsDestination = layout.buildDirectory.dir("compose_compiler") } }
Or the always-on form:
kotlincomposeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") metricsDestination = layout.buildDirectory.dir("compose_compiler") // Optional — opt mutable third-party types into stability: stabilityConfigurationFiles.add( rootProject.layout.projectDirectory.file("stability_config.conf") ) }
bash./gradlew :app:assembleRelease -PcomposeCompilerReports=true
For a library module: ./gradlew :feature-feed:assembleRelease. The release flavor is required — see RIGHT/WRONG below.
<module>/build/compose_compiler/:app/build/compose_compiler/
├── app_release-classes.txt # per-class stability
├── app_release-composables.txt # per-composable signatures
├── app_release-composables.csv # CSV mirror of the above (CI-friendly)
└── app_release-module.json # aggregate countsIf any file is missing, the plugin did not run for that variant — re-check that composeCompiler { reportsDestination = ... } is on the right module and that the build was release.
composables.txt first. This is the highest-signal file. Search for restartable lines that are not followed by skippable. Each one is a recomposition entry point that cannot be skipped. Inside each block, read the per-parameter prefix: stable, unstable, @static, @dynamic. Any unstable parameter blocks skipping. See references/reading-composables-txt.md for the full grammar.classes.txt to learn _why_ a class is unstable. For every type flagged unstable in composables.txt, find its declaration in classes.txt. The line tells you whether a var field, a generic parameter, or an unstable nested type is the cause. The runtime stable class Box { stable val value: T } shape means "this class is stable iff the runtime $stable: Int field of the substituted T says so" — see references/reading-classes-txt.md.module.json for triage numbers. Counts of skippable composables, restartable composables, stable classes, etc. Use this to compare before/after a fix or to decide which module to attack first. DO NOT treat these counts as a target — they exist to spot regressions, not to chase 100 percent.LazyColumn item is critical. Cross-reference with measurement (@TraceRecomposition from skydoves/compose-stability-analyzer, or Macrobenchmark FrameTimingMetric) before fixing — see ../../measurement/tracing-recompositions-at-runtime/SKILL.md.../stabilizing-compose-types/SKILL.md.The compiler reports surface the cause directly inside the function signature. Walk the developer through this real shape.
textrestartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks( stable index: Int, unstable snacks: List<Snack>, // <-- blocks skipping stable onSnackClick: Function1<Long, Unit>, )
Diagnosis script:
restartable but NOT prefixed with skippable. Therefore it always recomposes when its parent does.unstable snacks: List<Snack> parameter. kotlin.collections.List is an interface; the compiler cannot prove its implementations are immutable.classes.txt and find Snack. If Snack itself is unstable, fix the data class first; if it is stable then only the List wrapper is the problem.List<Snack> with kotlinx.collections.immutable.ImmutableList<Snack>, or add kotlin.collections.* to stability_config.conf if the developer is comfortable with that contract.runtime meanstextruntime stable class Box { stable val value: T }
This is not unstable. The compiler emits a synthetic $stable: Int field at runtime and queries it during composition. The class is stable iff the substituted T reports stable. DO NOT annotate runtime classes with @Stable to "promote" them — the runtime check is the correct mechanism.
bash# WRONG ./gradlew :app:assembleDebug # WRONG because: debug enables Live Literals; constant 0 dp becomes a getter, every literal looks dynamic, and counts in module.json drift versus what ships to users.
bash# RIGHT ./gradlew :app:assembleRelease -PcomposeCompilerReports=true
text// WRONG — reading composables.txt without checking the leading flags fun MyScreen(...) // WRONG because: skipping the `restartable`/`skippable` prefix discards the only data point that determines whether unstable params actually cost anything.
text// RIGHT — every diagnosis quotes the full prefix and per-param annotations restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MyScreen( stable user: User, stable onClick: Function0<Unit>, )
If build/compose_compiler/ is missing or empty after a release build:
org.jetbrains.kotlin.plugin.compose plugin is applied to this module — the extension is per-module../gradlew :app:clean :app:assembleRelease.reportsDestination is set inside a composeCompiler { } block, not the legacy kotlinOptions freeCompilerArgs flow.composables.txt per-parameter annotations (stable/unstable/@static/@dynamic), not just the function name. The flag prefix is the diagnosis.classes.txt to identify the root cause (a var, a generic, an unstable field type, or an interface).@Stable based on a report alone — that decision belongs to the fix skill, which evaluates the contract.composables.csv line counts and call it done. The counts are a regression sentinel; the per-line annotations are the actual diagnosis.../enforcing-stability-in-ci/SKILL.md (skydoves compose-stability-analyzer plugin or the community ComposeGuard plugin) so regressions surface on PR review.module.json per release so future regressions are visible by diff../gradlew :app:assembleRelease (or module-specific) completes successfully with the org.jetbrains.kotlin.plugin.compose plugin applied.<module>_release-classes.txt, <module>_release-composables.txt, <module>_release-composables.csv, <module>_release-module.json.composables.txt, OR a confirmed-zero count from module.json justifying that no fix is needed.classes.txt so the fix skill receives a concrete root cause (a var, a List, a LocalDateTime, etc.).runtime stable class … is not a problem — it is the compiler's correct lazy-stability emission.references/reading-classes-txt.md — full grammar for classes.txt with worked examples (stable / unstable / runtime / generic).references/reading-composables-txt.md — full grammar for composables.txt including restartable, skippable, readonly, scheme, and per-parameter stable / unstable / @static / @dynamic.Other measured skills in the registry, with their headline benchmark lift.