Compose Compiler Stability Inference System
Compose Compiler Stability Inference System
A comprehensive study of how the Compose compiler determines type stability for recomposition optimization.
Table of Contents
- Compose Compiler Stability Inference System
Chapter 1: Foundations
1.1 Introduction
The Compose compiler implements a stability inference system to enable recomposition optimization. This system analyzes types at compile time to determine whether their values can be safely compared for equality during recomposition.
Source File: compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/analysis/Stability.kt
The inference process involves analyzing type declarations, examining field properties, and tracking stability through generic type parameters. The results inform the runtime whether to skip recomposition when parameter values remain unchanged.
1.2 Core Concepts
Stability Definition
A type is considered stable when it satisfies three conditions:
- Immutability: The observable state of an instance does not change after construction
- Equality semantics: Two instances with equal observable state are equal via
equals() - Change notification: If the type contains observable mutable state, all state changes trigger composition invalidation
These properties allow the runtime to make optimization decisions based on value comparison.
Recomposition Mechanics
When a composable function receives parameters, the runtime determines whether to execute the function body:
@Composable
fun UserProfile(user: User) {
// Function body
}
The decision process:
- Compare the new
uservalue with the previous value - If equal and the type is stable, skip recomposition
- If different or unstable, execute the function body
Without stability information, the runtime must conservatively recompose on every invocation, regardless of whether parameters changed.
1.3 The Role of Stability
Performance Impact
Stability inference affects recomposition in three ways:
Smart Skipping: Composable functions with stable parameters can be skipped when parameter values remain unchanged. This reduces the number of function executions during recomposition.
Comparison Propagation: The compiler passes stability information to child composable calls, enabling nested optimizations throughout the composition tree.
Comparison Strategy: The runtime selects between structural equality (equals()) for stable types and referential equality (===) for unstable types, affecting change detection behavior.
Consider this example:
// Unstable parameter type - interface with unknown stability
@Composable
fun ExpensiveList(items: List<String>) {
// List is an interface - has Unknown stability
// Falls back to instance comparison
}
// Stable parameter type - using immutable collection
@Composable
fun ExpensiveList(items: ImmutableList<String>) {
// ImmutableList is in KnownStableConstructs
// Can skip recomposition when unchanged
}
// Alternative: Using listOf() result
@Composable
fun ExpensiveList(items: List<String>) {
// If items comes from listOf(), the expression is stable
// But the List type itself is still an interface with Unknown stability
}
The key insight: List and MutableList are both interfaces with Unknown stability. To achieve stable parameters, use:
ImmutableListfrom kotlinx.collections.immutable (in KnownStableConstructs)- Add
kotlin.collections.Listto your stability configuration file - Use
@Stableannotation on your data classes containing List
Chapter 2: Stability Type System
2.1 Type Hierarchy
The compiler represents stability through a sealed class hierarchy defined in :
sealed class Stability {
class Certain(val stable: Boolean) : Stability()
class Runtime(val declaration: IrClass) : Stability()
class Unknown(val declaration: IrClass) : Stability()
class Parameter(val parameter: IrTypeParameter) : Stability()
class Combined(val elements: List<Stability>) : Stability()
}
Each subtype represents a different category of stability information available to the compiler.
2.2 Compile-Time Stability
Stability.Certain
This type represents stability that can be determined completely at compile time.
Structure:
class Certain(val stable: Boolean) : Stability()
The stable field indicates whether the type is definitely stable (true) or definitely unstable (false).
Examples:
// Certain(stable = true)
class Point(val x: Int, val y: Int)
// Certain(stable = false)
class Counter(var count: Int)
Usage Conditions:
- Primitive types (
Int,Long,Boolean, etc.) StringandUnit- Function types (
FunctionN,KFunctionN) - Classes with only stable
valproperties - Classes with any
varproperty (immediately unstable) - Classes marked with
@Stableor@Immutableannotations
Implementation: See for the knownStable() extension function.
2.3 Runtime Stability
Stability.Runtime
This type indicates that stability must be checked at runtime by reading a generated $stable field.
Structure:
class Runtime(val declaration: IrClass) : Stability()
The declaration references the class whose stability requires runtime determination.
Generated Code Example:
// Source code
class Box<T>(val value: T)
// Compiler-generated code
@StabilityInferred(parameters = 0b1)
class Box<T>(val value: T) {
companion object {
@JvmField
val $stable: Int = /* computed based on type parameters */
}
}
When Applied:
- Classes from external modules (separately compiled)
- Generic classes where type parameters affect stability
- Classes with
@StabilityInferredannotation
Runtime Behavior:
At instantiation sites, the runtime computes the $stable field value:
Box<Int> // $stable = STABLE (0b000)
Box<MutableList> // $stable = UNSTABLE (0b100)
Implementation: See and .
2.4 Uncertain Stability
Stability.Unknown
This type represents cases where the compiler cannot determine stability.
Structure:
class Unknown(val declaration: IrClass) : Stability()
Examples:
interface Repository {
fun getData(): String
}
class Screen(val source: Repository)
// Repository has Unknown stability
Usage Conditions:
- Interface types (unknown implementations)
- Abstract classes without concrete analysis
- Types in incremental compilation scenarios
Runtime Behavior:
When encountering Unknown stability, the runtime falls back to instance comparison (===) for change detection. This conservative approach ensures correctness but prevents skipping optimizations.
Implementation: See .
2.5 Parametric Stability
Stability.Parameter
This type represents stability that depends on a generic type parameter.
Structure:
class Parameter(val parameter: IrTypeParameter) : Stability()
Example:
class Wrapper<T>(val value: T)
// ^^^^^^^^^^^^
// Stability depends on T
// Instantiation examples:
Wrapper<Int> // Stable (Int is stable)
Wrapper<Counter> // Unstable (Counter from 2.2 is unstable)
Resolution Process:
When analyzing Wrapper<Int>:
- Identify
value: ThasStability.Parameter(T) - Substitute
TwithInt - Evaluate
stabilityOf(Int)=Stable - Result:
Wrapper<Int>is stable
Implementation: See for type parameter handling.
2.6 Combined Stability
Stability.Combined
This type aggregates multiple stability factors from different sources.
Structure:
class Combined(val elements: List<Stability>) : Stability()
Examples:
class Complex<T, U>(
val primitive: Int, // Certain(stable = true)
val param1: T, // Parameter(T)
val param2: U // Parameter(U)
)
// Combined([Certain(true), Parameter(T), Parameter(U)])
Combination Rules:
The compiler combines stabilities using the plus operator ():
Stable + Stable = Stable
Stable + Unstable = Unstable
Unstable + Stable = Unstable
Stable + Parameter = Combined([Parameter])
Parameter + Parameter = Combined([Parameter, Parameter])
Runtime + Parameter = Combined([Runtime, Parameter])
Key Property: Unstable stability dominates all combinations. A single unstable component makes the entire result unstable.
2.7 Stability Decision Tree
The Compose compiler follows a systematic decision tree when determining stability. This tree represents the actual logic flow implemented in the compiler.
Complete Decision Tree
┌─────────────────────────────────┐
│ Start: Analyze Type/Class │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Is it a primitive type? │───Yes──→ [STABLE]
│ (Int, Boolean, Float, etc.) │
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it String or Unit? │───Yes──→ [STABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it a function type? │───Yes──→ [STABLE]
│ (Function<*>, KFunction<*>) │
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Has @Stable or @Immutable? │───Yes──→ [STABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Has @StableMarker descendant? │───Yes──→ [STABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it an Enum class/entry? │───Yes──→ [STABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it a Protobuf type? │───Yes──→ [STABLE]
│ (GeneratedMessage/Lite) │
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it in KnownStableConstructs?│───Yes──→ [STABLE/RUNTIME]
│ (Pair, Triple, etc.) │ (check type params)
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Matches external config? │───Yes──→ [STABLE/RUNTIME]
│ (stability-config.conf) │ (check type params)
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it an interface? │───Yes──→ [UNKNOWN]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Is it from external Java? │───Yes──→ [UNSTABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Has @StabilityInferred? │───Yes──→ [RUNTIME]
│ (from separate compilation) │ (use bitmask)
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ Analyze class members: │
│ - Check all properties │
│ - Check backing fields │
│ - Check superclass │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Any var (mutable) property? │───Yes──→ [UNSTABLE]
└────────────┬────────────────────┘
│ No
▼
┌─────────────────────────────────┐
│ All members stable? │───Yes──→ [STABLE]
└────────────┬────────────────────┘
│ No
▼
[UNSTABLE/COMBINED]
Decision Tree for Generic Types
When analyzing generic types, the compiler follows an additional decision path:
┌─────────────────────────────────┐
│ Generic Type: Class<T1, T2> │
└────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Base class stable? │───No───→ [UNSTABLE]
└────────────┬────────────────────┘
│ Yes
▼
┌─────────────────────────────────┐
│ Has stability bitmask? │───No───→ Analyze each
│ (from KnownStableConstructs │ type parameter
│ or external config) │ individually
└────────────┬────────────────────┘
│ Yes
▼
┌─────────────────────────────────┐
│ For each type parameter Ti: │
│ Is bit i set in bitmask? │───No───→ Ti doesn't affect
└────────────┬────────────────────┘ stability
│ Yes
▼
┌─────────────────────────────────┐
│ Check stability of actual │
│ type argument for Ti │───→ Combine all results
└─────────────────────────────────┘
This article continues for subscribers
Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.
Become a Sponsor