Repository navigation
Upgrading
This release requires Kotlin 2.4.20, unchanged from 0.14.0.
Verdicts change for real code, so run ./gradlew stabilityDump once after upgrading and commit the result. The corrections move some parameters in both directions: STABLE -> UNSTABLE where a class inherits from an unstable base, STABLE -> UNKNOWN for sealed classes, and UNSTABLE -> STABLE for state holders built on delegated vars.
Two further changes need no action but are worth knowing about. Kotlin compilation in consuming projects is shareable through a remote build cache again: the report directory used to put the absolute checkout path into every compile task's cache key, so entries never matched between machines or worktrees. And the IntelliJ plugin loads on 2024.2, 2024.3 and 2025.1 again, which 0.14.0 does not.
Fixed
unstableOnlyno longer drops composables thatstabilityCheckthen reports. The baseline was filtered byskippablealone, whilecompareStabilitydecides whether a new composable is a regression withhasUnstableParameter(#192). Under strong skipping those disagree constantly: a composable stays skippable while a parameter isUNSTABLE,RUNTIMEorUNKNOWN, so the entry was written out of the baseline and reported as a new unstable composable on the very next check, with no way to accept it becausestabilityDumpdropped it again. The baseline is now filtered by the union issue #128 asked for, recording a composable when it has an unstable parameter, or is not skippable, or is not restartable. That is a superset of the predicate the check reports new composables on, so a dumped baseline can no longer report its own code. On one 54-module project, 296 of 1,543 entries were affected.- The report directory no longer breaks Gradle build cache relocatability. The output directory was passed to the compiler as a plain
SubpluginOption, and KGP exposes every plain option throughCompilerPluginConfig.getAsTaskInputArgs(), which is annotated@Input. The absolute checkout path therefore became part of the build cache key of every Kotlin compilation in a consuming module, so remote cache entries never matched between machines or worktrees andcompile*Kotlinwas never reused. It is now aFilesSubpluginOption, which KGP excludes from those input args while still passing the path to the compiler. Measured across two checkouts sharing one cache: a miss before,FROM-CACHEafter, with the report restored. The report directory is also declared as a task output for the JS and metadata compilations, not only the JVM one, because they became relocatable at the same time and would otherwise hit the cache with nothing restoring their report. Kotlin/Native is unaffected by the fix:KotlinNativeCompileexposes plugin options as a plain@Inputcommand line that no option subtype can keep out of the cache key. (#212) unstableOnlynow says so when it needsignoreNonRegressiveChanges. The filtered entries are absent from the baseline, and without that optionstabilityCheckreports every missing composable as a new one, which fails the build just as permanently.stabilityDumpnow warns, naming the option and the number of entries it dropped, instead of leaving the combination to fail silently.
Stability inference is now checked rule-by-rule against the Compose compiler's own analysis/Stability.kt, and several rules were wrong. The sample app is verified against the compiler's own metrics output: 0 skippable and 0 restartable disagreements across 49 composables.
- A delegated
varno longer makes a class unstable. Compose exempts it (if (member.isVar && !member.isDelegated) return Unstable) and scores the delegate instead, so the canonical state holder is stable:This told users the correct Compose pattern was broken. Properties are now scored by their backing field type, as Compose does, soclass UiState { var text by mutableStateOf("") } // was UNSTABLE, Compose says stable
var x by mutableStateOf(...)is seen as the@Stable MutableStateit is. @StabilityInferredwas decoded backwards. Theparametersvalue is a bitmask, not a boolean: bits0..n-1mark which type parameters the stability depends on and bitnis a "known stable" sentinel, so for a class with no type parameters1means stable and0means not stable. Reading it as== 0 -> STABLEreported cross-module classes the compiler had marked unstable as STABLE, the one direction that makes a user do nothing when they should act. Fixed in the compiler and at both IDE sites.- Open, abstract and sealed classes no longer short-circuit to
UNKNOWN. The Compose compiler has no such rule;modality == FINAL ? Stable : Unknownis the seed of its member loop, so a non-final class still becomes unstable from avaror an unstable member and still reaches the cross-module rules. Short-circuiting skipped all of that:androidx.lifecycle.ViewModelreportedUNKNOWNwhere the compiler says unstable, and a sealed class with no stored properties reported STABLE where the compiler says uncertain. - A superclass now contributes unconditionally. It was only consulted when the subclass declared no state of its own, so
class Foo : ViewModel() { val a = "" }reported STABLE while the compiler reported unstable. Only the single class supertype is considered, matching Compose; scanning every supertype made the verdict depend on the order they were written (class A : Marker, ViewModel()andclass A : ViewModel(), Markerdisagreed). objectdeclarations are stable, a rule that was missing entirely. A singleton's identity never changes, so its properties cannot destabilize a parameter of its type.varargparameters are scored by their element type, not the synthesizedArray<out T>, which is a stdlib stub and made every vararg parameter UNSTABLE. Applied to both the report and the recomposition-tracing path, which must agree because the log line is a wire protocol the IDE parses.- Suspend function types are no longer claimed stable. Compose's stable shortcut covers ordinary and
@Composablefunction types only, so akotlin.coroutines.SuspendFunctionNis analysed as the interface it is. - The IDE now resolves
@StableMarkeras a rule, walking supertypes, so a project's own marker annotation works there as it already did in the compiler, and it recognisesobjectdeclarations and delegatedvars the same way. stabilityConfigurationFilesreaches the compiler again. The compiler read the top-level Gradle property while the dump and check tasks read the deprecated nested one, and the README documents the nested form: a configuration written the documented way was honoured bystabilityCheckbut silently ignored by the compiler's own inference. Both forms are now merged for every consumer.
Known gap
A user-defined generic class is still reported RUNTIME regardless of its type argument. The Compose compiler records such a class as Stability.Parameter(T) and substitutes the actual argument at each use site, so UiResult<Unit> is stable there and merely RUNTIME here. Implementing that needs an argument-aware recursion guard and is tracked separately.