debugging-recompositions — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited debugging-recompositions (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Recomposition is invisible by default. A composable that re-runs sixty times a second to redraw an animation looks identical in source to one that should re-run zero times when its parent ticks. Three layers of instrumentation make it visible: Layout Inspector recomposition counts and skip counts (Android Studio, debug build), Layout Inspector Argument Change Reasons (Hedgehog and later, per-parameter Changed / Unchanged / Uncertain / Static / Unknown classifications), and runtime `@TraceRecomposition` from compose-stability-analyzer for release / R8 / real-device measurement.
This skill is the diagnostic layer. It does not fix recompositions; it tells the developer which composable is recomposing and which parameter is responsible. Once the param is named, the fix lives in a sibling skill — stability for unstable types, strong skipping for lambda capture, or phase deferral for state reads in the wrong phase.
../../stability/diagnosing-compose-stability/SKILL.md and ../../stability/stabilizing-compose-types/SKILL.md.../using-strong-skipping-correctly/SKILL.md.../deferring-state-reads/SKILL.md.../../stability/enforcing-stability-in-ci/SKILL.md.androidx.lifecycle:lifecycle-runtime-compose available so collectAsStateWithLifecycle can replace plain collectAsState when a flow turns out to be the offender.../../measurement/tracing-recompositions-at-runtime/SKILL.md (the compose-stability-analyzer runtime + ComposeStabilityAnalyzer.setEnabled(true) in Application.onCreate).../../stability/diagnosing-compose-stability/SKILL.md) — Layout Inspector classifications mirror the same stability vocabulary.Run the app on a connected device or emulator in debug. Open Android Studio → Tools → Layout Inspector. In the Layout Inspector toolbar, toggle:
Both are off by default. With both on, the tree displays <recompositions>/<skips> per node, which is the primary signal.
The healthy ratio is "skips ≫ recompositions". A composable with 100/0 (one hundred recompositions, zero skips) is the smoking gun — every parent tick re-ran its body without any skip guard firing. A composable with 100/95 is doing fine — only five recompositions triggered actual work.
Counts are cumulative. Reset them via the Layout Inspector "Reset" button before reproducing. Then perform the user action that the developer suspects is causing the jank — scroll, tap, animation tick — and watch which composables' counts climb.
The composable with the disproportionate count growth is the suspect. Note its name and parameter list before moving on.
Click the suspect composable in the Layout Inspector tree. The right-hand panel now shows a "Recomposition reasons" section listing each parameter with one of five classifications:
| Status | Meaning | Likely action |
|---|---|---|
| Static | Compile-time constant; never invalidates. | None. This param is not the cause. |
| Unchanged | Value compared equal to the previous composition (same identity or equals). | None. This param did not trigger the recomposition. |
| Changed | Value compared not-equal to the previous composition. | Investigate whether the change was meaningful or accidental (e.g. a copy() with identical content failing equals because a field is MutableList). |
| Uncertain | Compose cannot determine whether the parameter changed since the last composition; typically the param type is unstable to the compiler so the runtime fell back to identity equality (===) and the comparison was inconclusive. | Stabilize the type (annotate with @Stable / @Immutable, or replace List<T> with ImmutableList<T> / PersistentList<T> so structural equals applies) — or accept the recomposition. |
| Unknown | The compiler could not statically classify the type's stability. The runtime fell back to ===. | Run ../../stability/diagnosing-compose-stability/SKILL.md to find why the type is unclassified — usually a separately-compiled module without @StabilityInferred, an interface, or an unannotated POJO from a Java library. |
The status names what to do next; do not jump to a fix without naming the status.
Decision tree once a status is identified:
copy() produce a structurally-different object the developer thought was identical? Common offender: a Flow collected without distinctUntilChanged() emitting equal values that fail reference equality.../../stability/stabilizing-compose-types/SKILL.md. Frequently the smoking gun for an instance-equality miss after a data class copy().../../stability/diagnosing-compose-stability/SKILL.md. Most common causes: interface-typed parameter, separately-compiled Java POJO, generic parameter with no instance to substitute.Layout Inspector counts come from a debug build. Debug builds have Live Literals (constant values become getters), interpreted Compose runtime, and no R8. Counts are useful as a directional signal but not as a final number. For release-mode confirmation, instrument the suspect composable with @TraceRecomposition from compose-stability-analyzer and re-run the scenario on a release + R8 build. Cross-link ../../measurement/tracing-recompositions-at-runtime/SKILL.md for the full setup; the two-line summary:
@TraceRecomposition(traceStates = true)
@Composable
fun SnackRow(snack: Snack, onClick: (Long) -> Unit) { /* ... */ }Logcat under Recomposition tag will print one line per recomposition, with each parameter and whether it changed. The release-mode count is the ground truth; quote it (not the debug count) when reporting that a fix worked.
# WRONG
"Layout Inspector shows SnackRow at 100/0 in debug — fix shipped."
# WRONG because: debug builds run interpreted with Live Literals turning constants into
# getters that defeat compile-time folding and inflate recomposition. The same composable
# in release + R8 may run 5/95. Always confirm the final number in release with @TraceRecomposition.# RIGHT
"Layout Inspector showed SnackRow at 100/0 in debug. After the fix, debug shows 5/95
and release-mode @TraceRecomposition confirms 0 recompositions across a 30-frame scroll."See ../../measurement/testing-compose-in-release-mode/SKILL.md for why debug counts lie.
# WRONG
"FilterBar is recomposing too much. I'll mark Filter @Stable and see if it helps."
# WRONG because: the developer skipped naming the Argument Change Reason. If the status was
# Unknown, the type needs classification (probably from an external module); if it was
# Uncertain, the type needs @Immutable + ImmutableList; if it was Changed, the upstream
# producer is mutating where it shouldn't. Each requires a different fix; guessing wastes a cycle.# RIGHT
"FilterBar is recomposing per-frame. Layout Inspector reports the `filter` param as Uncertain.
The Filter type holds List<String>; replacing with ImmutableList<String> + @Immutable should
let equals() catch structural equality. Confirm with @TraceRecomposition in release.""Uncertain" is the single most diagnostically-rich status. It says: "the compiler thought this type was stable, the runtime ran the comparison, and identity differed." That is almost always a copy() or a builder producing structurally-equal-but-identity-different instances, plus an equals that does not apply because a sub-field is unstable.
// WRONG — declared @Immutable but the field is mutable
@Immutable
data class Filter(val tags: List<String>, val sort: SortOrder)
// WRONG because: List<String> is unstable; @Immutable is a contract the developer broke.
// Layout Inspector reports `filter` as Uncertain on every recompose; structural equals
// does not run because a sub-field is unstable.// RIGHT
@Immutable
data class Filter(val tags: ImmutableList<String>, val sort: SortOrder)// RIGHT — annotate the suspect, run release with R8, capture logcat
@TraceRecomposition(traceStates = true)
@Composable
fun FilterBar(filter: Filter) { /* ... */ }adb logcat -s Recomposition:D
# Expected after the fix: a single "[Recomposition #1]" on initial composition,
# then no further entries during the scroll scenario.See ../../measurement/tracing-recompositions-at-runtime/SKILL.md for the full instrumentation skill.
A composable at 0/0 is dead code or never reached. A composable at 100/0 is broken. A composable at 100/95 is healthy — only five real recompositions out of one hundred parent ticks. The skip-to-recomposition ratio is the metric to track over time, not the absolute count.
filter is Uncertain on every parent tick" is.@TraceRecomposition.../../stability/diagnosing-compose-stability/SKILL.md, Changed (lambda capture) → ../using-strong-skipping-correctly/SKILL.md, Changed (state read in wrong phase) → ../deferring-state-reads/SKILL.md.@Immutable contract violated by a mutable sub-field.../../measurement/tracing-recompositions-at-runtime/SKILL.md for end-to-end release confirmation.Recomposition is invisible by default; the Layout Inspector + Argument Change Reasons + @TraceRecomposition triple is the diagnostic stack. Skippability remains a diagnostic, not a KPI — the goal is "no surprising recompositions on the user-perceived hot path", not "zero recompositions everywhere".
../../stability/enforcing-stability-in-ci/SKILL.md) updated if applicable.@TraceRecomposition in a release + R8 build confirms the post-fix recomposition count matches the developer's expectation. Debug numbers were directional; this is the final number.@TraceRecomposition) — https://github.com/skydoves/compose-stability-analyzerFor the upstream stability fix when the status is Uncertain or Unknown, see ../../stability/diagnosing-compose-stability/SKILL.md and ../../stability/stabilizing-compose-types/SKILL.md. For lambda-capture causes of Changed, see ../using-strong-skipping-correctly/SKILL.md. For confirming a fix in a release + R8 build, see ../../measurement/tracing-recompositions-at-runtime/SKILL.md and ../../measurement/testing-compose-in-release-mode/SKILL.md. For preventing future regressions, see ../../stability/enforcing-stability-in-ci/SKILL.md.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.