Skip to content
All Skills

Understanding Stability Inference

Use this skill to explain why the Compose compiler classified a class or composable parameter as stable, runtime, unknown, or unstable. Covers the 12-phase inference algorithm, the five compiler-level stability types (Certain / Runtime / Unknown / Parameter / Combined), the generic bitmask encoding (Pair=0b11, ImmutableList=0b1), the Known Stable Constructs registry, and the runtime `$stable: Int` field generated by `@StabilityInferred`. Use when the developer asks "why is X classified as Y?", when a stability report shows a surprising `runtime stable`, `unknown`, or `unstable` verdict, when generics, inheritance, cycles, interfaces, or cross-module classes are involved, or when the user mentions `$stable`, `@StabilityInferred`, separate compilation, or "the compiler thinks my class is unstable but it looks fine".

Software Engineering|v1|Updated 7/14/2026|GitHub source
MCP get_skill({ skillId: "understanding-stability-inference-66373ecd" })

Use this skill with your agent

Create a free account and connect via MCP

Get Started Free
# Understanding Stability Inference — read the compiler's mind

Stability is decided by a 12-phase algorithm baked into the Compose compiler. This skill teaches Claude how the algorithm walks a type so it can explain *why* a report says what it says, and predict classifications before the report is even generated. Pair this with `../diagnosing-compose-stability/SKILL.md` (which generates the report) and `../stabilizing-compose-types/SKILL.md` (which fixes obvious unstable types). Reach for this skill when the simpler skills produce a verdict that surprises the developer.

## When to use this skill

- The developer asks "why is `Foo` classified as `runtime stable` and not `stable`?"
- A report shows `runtime` or `unknown` for a class that "looks fine".
- Generics involved: `Box<T>`, `Wrapper<A, B>`, `Pair<String, Int>`, `ImmutableList<User>`.
- The class lives in another module and ships as a `.class`/`.kotlin_metadata` artifact.
- The developer asks about the `$stable: Int` field, `@StabilityInferred`, or cross-module classification.
- A self-referential type (`class Node(val children: List<Node>)`) is unstable for non-obvious reasons.
- A Java type, an interface, or an abstract base appears in a parameter list and surprises the developer.

## When NOT to use this skill

- The fix is mechanical (`var` → `val`, `List` → `ImmutableList`, `Flow` parameter removal). Use `../stabilizing-compose-types/SKILL.md`.
- No report exists yet. Run `../diagnosing-compose-stability/SKILL.md` first.
- The developer wants CI enforcement of stability. Use `../enforcing-stability-in-ci/SKILL.md`.

## Prerequisites

- Compose Compiler reports already generated, or at least one `<module>-classes.txt` and `<module>-composables.txt` available in `build/compose_compiler/`.
- The developer understands the basic stability vocabulary: `stable`, `unstable`, `skippable`, `restartable`, `@Stable`, `@Immutable`.
- Kotlin 2.0+ with the Compose compiler plugin (Strong Skipping default ON).

## Workflow — diagnostic question and answer tree

Walk the type through the same 12 phases the compiler does. For each call site, ask the questions in order; the first matching phase wins.

The canonical phase order (used everywhere in this skill and matching `references/twelve-phase-algorithm.md`):

1. **Phase 1 — primitive / `String` / function / `Unit` fast path → `Stable`.**
   The compiler returns immediately. No field analysis runs. Mention the fast path so the developer knows nothing else was inspected.
2. **Phase 2 — type parameter substitution.**
   A bare type variable `T` becomes `Stability.Parameter(T)`; resolution is deferred to the call site that substitutes it.
3. **Phase 3 — nullable unwrap (`Int?` → analyze `Int`).**
   Nullability does not change stability; the algorithm strips the `?` and recurses.
4. **Phase 4 — inline class — check underlying type.**
   `value class Wrapper(val raw: T)` is exactly as stable as `T`.
5. **Phase 5 — cycle detection (recursive trees → conservative UNSTABLE).**
   The algorithm bails on cycles to guarantee termination. The escape hatch is `@Stable`/`@Immutable` on the recursive class, which fires in phase 6 before phase 5 is reached.
6. **Phase 6 — annotations check (`@Stable`, `@Immutable`, `@StableMarker`).**
   Yes → `Stability.Certain` (stable). `@Immutable` enables additional optimizations beyond `@Stable` because the compiler can promote reads of properties to static expressions and elide equality checks; `@Stable` only promises change notification.
7. **Phase 7 — Known Stable Constructs registry hit** (`Pair` / `Triple` / `Result` / `ImmutableList` / `dagger.Lazy` / `ClosedRange` / etc.).
   Returns `Stability.Parameter` with the registry's bitmask. See `references/bitmask-encoding.md` for the full registry.
8. **Phase 8 — external configuration match** (`stability_config.conf`).
   Returns `Stability.Parameter` with the bitmask declared in the config file.
9. **Phase 9 — external module (`@StabilityInferred` annotation generated by separate compilation).**
   Returns `Stability.Runtime`. The compiler emits a `$stable: Int` field on the JVM (a mangled top-level property on Native/JS) that the **runtime** queries via `Composer.changed`. Tell the developer this is **not a bug** — `runtime` is the compiler saying "I cannot prove this at compile time, so I will check at runtime".
10. **Phase 10 — Java type (default UNSTABLE — fix via config file).**
    Java `final` fields look like `var` to the inference because the algorithm has no Kotlin metadata to read. Fix via `stabilityConfigurationFiles`, not by editing the Java source.
11. **Phase 11 — interface (UNKNOWN; runtime `===`).**
    The compiler cannot enumerate implementations from a single call site; the runtime falls back to identity (`===`) for the equality probe.
12. **Phase 12 — field-by-field analysis** (the slow path):
    - Walks the **linearized class hierarchy** so inherited fields participate. There is no separate "inheritance" phase — inheritance lives here.
    - Any `var` property → `Unstable` (mutation observed without Snapshot integration).
    - Any property whose type is `Unstable` → `Unstable` (Combined dominates).
    - Otherwise the class is `Stable` (Combined of all-stable fields collapses to Stable).

For full pseudocode of all 12 phases plus the field-by-field loop, see `references/twelve-phase-algorithm.md`.

## Patterns

### Pattern: "Why does `Box<String>` show as `runtime stable`?"

```kotlin
// Source — the developer's class, in a library module
class Box<T>(val value: T)

// Call site, in app module
@Composable fun BoxRow(box: Box<String>) { Text(box.value) }
```

The compiler walks `Box<T>`:

1. Phase 12 (field-by-field) inside the defining module finds one field `value: T`; recursion on `T` hits phase 2 (type parameter) → `Stability.Parameter`. Combined collapses to `Stability.Parameter` with bitmask `0b1` (the single type parameter affects stability).
2. Because `Box` is consumed from a different module, the compiler emits `@StabilityInferred(parameters = 0b1)` on `Box` and a `$stable: Int` field initialized from `T`'s stability at runtime. Downstream call sites pick this up via phase 9.
3. Call-site substitution → `T = String` → String is Certain Stable → bit 0 satisfied.
4. Final report line: `runtime stable class Box<T>` and at the call site `BoxRow` is `skippable`.

```kotlin
// WRONG mental model
// "runtime stable means there is a runtime cost on every recomposition" — partly true but misleading
// WRONG because: the cost is one Int field load and a bitwise AND, performed once when the runtime
// computes the call-site stability. It is far cheaper than the unskipped recomposition it prevents.
```

```kotlin
// RIGHT mental model
// runtime stable = "the compiler proved stability conditional on the type arguments, and emitted
// a $stable: Int field whose bits the runtime ANDs against the substituted argument stabilities".
// The skip decision is still made; it is just made at runtime instead of compile time.
```

### Pattern: "Why does `@Immutable data class Person(val name: String)` enable more optimizations than `@Stable`?"

`@Stable` is a contract: "I will notify Compose of changes". `@Immutable` is a stronger contract: "I will never change". With `@Immutable` the compiler may promote reads of `Person.name` to **static expressions** and elide equality probes for nested usages; with `@Stable` it must still emit equality checks. Both classify as `Stability.Certain`, but the downstream optimizer treats `@Immutable` more aggressively.

```kotlin
// WRONG
@Stable data class Coordinates(val lat: Double, val lng: Double)
// WRONG because: Coordinates never mutates after construction. @Stable understates the contract
// and forfeits static-expression promotion at every read site.
```

```kotlin
// RIGHT
@Immutable data class Coordinates(val lat: Double, val lng: Double)
```

### Pattern: "Why is my recursive tree unstable even though it looks fine?"

```kotlin
data class Node(val id: String, val children: List<Node>)
```

Phase 5 (cycle detection) bails. The compiler does not attempt fixed-point analysis because it would have to assume the answer to prove the answer. The conservative verdict is **Unstable**. Even if every field is otherwise stable, the recursion through `children` returns Unstable to the parent call.

```kotlin
// WRONG — adds @Stable to "force" stability
@Stable data class Node(val id: String, val children: List<Node>)
// WRONG because: List<Node> is a mutable interface backed by ArrayList in practice. The @Stable
// annotation tells the compiler to trust the contract, but the actual List instance can mutate
// between recompositions without notifying Compose, producing silent missed recompositions.
```

```kotlin
// RIGHT
import kotlinx.collections.immutable.ImmutableList
@Immutable data class Node(val id: String, val children: ImmutableList<Node>)
```

`ImmutableList` is in the **Known Stable Constructs registry** (phase 7) with bitmask `0b1`, so the recursion through `children` is permitted: cycle detection still triggers in phase 5, but the registry hit short-circuits the conservative verdict.

### Pattern: "Why does `Set<String>` block skipping but `ImmutableSet<String>` doesn't?"

`kotlin.collections.Set` is an interface (phase 11 → `Unknown`) backed in practice by `LinkedHashSet`, which mutates. `kotlinx.collections.immutable.ImmutableSet` is in the Known Stable Constructs registry with bitmask `0b1`, so it is `Stability.Parameter` and resolves to stable when the element type is stable.

```kotlin
// WRONG
@Composable fun TagRow(tags: Set<String>) { /* ... */ }
// WRONG because: Set is an interface — phase 11 returns Unknown, the call site is non-skippable.
```

```kotlin
// RIGHT
@Composable fun TagRow(tags: ImmutableSet<String>) { /* ... */ }
```

### Pattern: "Why is a class from another module `runtime stable` even when it has only `val`s?"

Separate compilation. At the time the call site compiles, the compiler does not have the full source AST of the dependency, only its `.class` files plus the metadata in `@StabilityInferred(parameters = ...)`. Phase 9 reads that annotation; the runtime resolves the bitmask against actual type arguments via the generated `$stable: Int` field. The classification is correct — there is no extra work to do — but it must be deferred to runtime because cross-module compile-time analysis is impossible without the source.

## Five compiler-level stability types

Cite these by name when answering "why" questions. The compiler stores stability as one of:

- **Stability.Certain** — primitives, String, Unit, function types, enums, `@Stable`/`@Immutable`-annotated classes. Decision is final and compile-time.
- **Stability.Runtime** — separately compiled class. Compile-time emits a `$stable: Int` field and `@StabilityInferred`; runtime ANDs the bits against actual type arguments.
- **Stability.Unknown** — interface, abstract class without concrete analysis, or anything the compiler refuses to commit on. Runtime falls back to `===` identity for the equality probe.
- **Stability.Parameter** — generic. Stability is a function of the type arguments via a bitmask.
- **Stability.Combined** — aggregate of multiple components (fields of a class, or multiple type arguments). **Unstable dominates** — any single Unstable component poisons the whole.

## Bitmask encoding (preview)

`Container<T1, T2, T3>` uses an Int bitmask where bit `i` set means `Ti` participates in stability:

| Type | Bitmask | Reading |
|---|---|---|
| `kotlin.Pair<A, B>` | `0b11` | both A and B affect stability |
| `kotlin.Triple<A, B, C>` | `0b111` | all three affect stability |
| `kotlinx.collections.immutable.ImmutableList<E>` | `0b1` | only E affects stability |
| `java.math.BigInteger` | `0b0` | no parameters; classified as stable regardless of erased type arguments |

The full rules — including how `@StabilityInferred(parameters = ...)` is generated for separately-compiled types and how the `$stable: Int` field is laid out on the JVM versus the mangled top-level property used on Kotlin/Native and Kotlin/JS — are in `references/bitmask-encoding.md`.

## Mandatory rules

- **MUST** teach the developer that `runtime stable` is not a bug or an unstable verdict — it is the compiler's way of saying "stability is conditional on type arguments and will be checked once at runtime via the `$stable: Int` field".
- **MUST NOT** suggest structural changes (changing `var` to `val`, swapping collection types) before explaining *why* the current structure is unstable. Diagnosis before treatment.
- **MUST** distinguish `Stability.Unknown` from `Stability.Unstable` when answering — `Unknown` means "cannot tell" and falls back to identity equality, `Unstable` means "proven unstable" and disables skipping outright.
- **MUST NOT** tell the developer to add `@Stable` to a type whose contract they cannot guarantee. A stability annotation is a contract; breaking it produces silent missed recompositions, which is worse than a non-skippable composable.
- **PREFERRED:** cite the Kotlin compiler source when depth helps. Concrete files: `Stability.kt` (the algebraic data type), `KnownStableConstructs.kt` (the registry), `ClassStabilityTransformer.kt` (the `$stable` field emission), and `StabilityConfigParser.kt` (the config-file reader).
- **PREFERRED:** when explaining a generic, walk the bitmask explicitly: "bit 0 of `Pair`'s bitmask is set, A=String is Certain Stable, satisfied; bit 1 is set, B=List is Unstable, fails — Combined collapses to Unstable".

## Verification

- [ ] Claude can predict, before running the report, whether a candidate type will be classified `Certain`, `Runtime`, `Unknown`, `Parameter`, or `Combined`.
- [ ] Claude can name which of the 12 phases produced the verdict.
- [ ] For a `runtime stable` class, Claude can explain that the compiler emitted `@StabilityInferred(parameters = ...)` on the class declaration and a `$stable: Int` field that the runtime ANDs against substituted type-argument stabilities.
- [ ] Claude refuses to recommend `@Stable` or `@Immutable` on a type whose mutation contract is not guaranteed.
- [ ] Claude correctly identifies `Set<T>`, `List<T>`, `Map<K, V>` as `Unknown` interfaces (not `Unstable`) when explaining why they block skipping.

## References

- Compose stability overview — https://developer.android.com/develop/ui/compose/performance/stability
- Stability — diagnose — https://developer.android.com/develop/ui/compose/performance/stability/diagnose
- Strong Skipping — https://developer.android.com/develop/ui/compose/performance/stability/strongskipping
- Ben Trengrove, "Jetpack Compose Stability Explained" — https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8
- Ben Trengrove, "New ways of optimizing stability" — https://medium.com/androiddevelopers/new-ways-of-optimizing-stability-in-jetpack-compose-038106c283cc
- Chris Banes, "Composable metrics" — https://chrisbanes.me/posts/composable-metrics/
- skydoves, "Optimize App Performance by Mastering Stability" — https://medium.com/proandroiddev/optimize-app-performance-by-mastering-stability-in-jetpack-compose-69f40a8c785d
- skydoves/compose-stability-inference — https://github.com/skydoves/compose-stability-inference
- skydoves/compose-stable-marker — https://github.com/skydoves/compose-stable-marker

### Reference files

- `references/twelve-phase-algorithm.md` — pseudocode walkthrough of all 12 phases plus the field-by-field analysis pseudocode.
- `references/bitmask-encoding.md` — generic stability bitmask rules, the Known Stable Constructs registry, the `@StabilityInferred(parameters = 0b1)` annotation generated by the compiler, the runtime `$stable: Int` field on JVM, and the mangled top-level property approach on Native and JS.
#android#jetpack-compose#performance#kotlin#code#quality

Related Skills

More skills in Software Engineering

Accessibility Standards

Comprehensive web accessibility standards based on WCAG 2.2 AA, with 38+ anti-patterns, legal enforcement context (EAA, ADA Title II), WAI-ARIA patterns, and framework-specific fixes for modern web frameworks and libraries.

#github-copilot#accessibilityMIT

Accord

Authoring unified specification packages across Business/Development/Design teams via staged elaboration (L0 Vision → L1 Requirements → L2 Team Detail → L3 Acceptance Criteria). No code. Use when authoring cross-team specs, building L0-L3 packages, or aligning Biz/Dev/Design on a single source of truth.

#broad-capability#developmentMIT

Acquire Codebase Knowledge

Use this skill when the user explicitly asks to map, document, or onboard into an existing codebase. Trigger for prompts like "map this codebase", "document this architecture", "onboard me to this repo", or "create codebase docs". Do not trigger for routine feature implementation, bug fixes, or narrow code edits unless the user asks for repository-level discovery.

#github-copilot#documentationMIT

Acreadiness Assess

Run the AgentRC readiness assessment on the current repository and produce a static HTML dashboard at reports/index.html. Wraps `npx github:microsoft/agentrc readiness` and hands off rendering to the @ai-readiness-reporter custom agent. Supports policies (--policy) for org-specific scoring. Use when asked to assess, audit, or score the AI readiness of a repo.

#github-copilot#planningMIT

Acreadiness Generate Instructions

Generate tailored AI agent instruction files via AgentRC instructions command. Produces .github/copilot-instructions.md (default, recommended for Copilot in VS Code) plus optional per-area .instructions.md files with applyTo globs for monorepos. Use after running /acreadiness-assess to close gaps in the AI Tooling pillar.

#github-copilot#skillMIT

Acreadiness Policy

Help the user pick, write, or apply an AgentRC policy. Policies customise readiness scoring by disabling irrelevant checks, overriding impact/level, setting pass-rate thresholds, or chaining org baselines with team overrides. Use when the user asks about strict mode, AI-only scoring, custom weights, CI gating, or wants org-wide standardisation.

#github-copilot#planningMIT

Explore Other Categories

Skills from other categories with shared topics

Avoiding Subcomposition Pitfalls

Use this skill when a Compose tree uses SubcomposeLayout, BoxWithConstraints, or Scaffold and the developer reports extra measure passes, slow first frame, or layout passes running content composition repeatedly. Covers why SubcomposeLayout composes its slots during the measure phase, why BoxWithConstraints forces a subcomposition for every new Constraints value, why nesting Scaffold or BoxWithConstraints multiplies the cost, when a custom Layout or Modifier.layout { } replaces SubcomposeLayout, and how to use SubcomposeLayoutState's slot reuse policy and precompose APIs when SubcomposeLayout is genuinely required. Use when the developer mentions BoxWithConstraints, SubcomposeLayout, Scaffold, "extra measure pass", "double measurement", "first frame slow", "subcompose", or notices that wrapping content in BoxWithConstraints regresses scroll perf inside a LazyColumn.

Mobile App Development#android#jetpack-compose

Choosing Derivedstateof

Use this skill to decide when Jetpack Compose derivedStateOf is the right tool and when it is pure overhead. Covers the "input frequency must exceed output frequency" rule, the mandatory remember { derivedStateOf { } } wrapper, the canonical pitfall of capturing non-state variables by initial value (and the remember(key) fix), and the snapshotFlow alternative for fire-and-forget side effects on derived values. Use when the developer mentions derivedStateOf, scroll-position-driven booleans, threshold checks, firstVisibleItemIndex, "show FAB on scroll", recomposition counts that don't drop after wrapping a value, or asks whether a computed string concatenation should use derivedStateOf.

Mobile App Development#android#jetpack-compose

Collecting Flows Safely

Use this skill to migrate Compose UI from `collectAsState()` to `collectAsStateWithLifecycle()`, hoist `Flow<T>` parameters out of composables, and apply `.conflate()` / `.distinctUntilChanged()` / `snapshotFlow` so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission. Covers ViewModel `StateFlow`/`SharedFlow` consumers, sensor and location streams, and the "Flow as composable parameter" antipattern. Trigger when the user mentions `collectAsState`, `collectAsStateWithLifecycle`, lifecycle-aware flow collection, `Lifecycle.State.STARTED`, background battery drain from a Compose screen, `snapshotFlow`, `Flow` parameter on a composable, conflate, or distinctUntilChanged.

Mobile App Development#android#jetpack-compose