Compose Performance & Stability Flashcards
UI › Compose UI
- What does it mean for a type to be 'stable' in Compose, and why does it matter?
- A stable type is one whose equals() is consistent and where Compose is notified of any observable change (e.g. via snapshot State). Stability lets the compiler mark composables as skippable, so Compose can skip recomposition when stable parameters are unchanged.
- Difference between @Stable and @Immutable?
- @Immutable promises the object's public properties never change after construction. @Stable allows mutation but promises Compose is notified of every change (and equals() is stable). Both are contracts that let the compiler treat the type as stable; violating them causes missed recompositions.
- Why is a List<T> parameter treated as unstable, and how do you fix it?
- List/Set/Map are interfaces, so the compiler can't prove the runtime instance is immutable. Fix it with kotlinx.immutable collections (ImmutableList/PersistentList), wrap it in an @Immutable data class, or rely on Strong Skipping Mode which makes the composable skippable anyway.
- What does Strong Skipping Mode change in Compose Compiler 2.0?
- It makes all restartable composables skippable even when some parameters are unstable (unstable params compared by reference equality ===, stable params by equals()), and it auto-remembers lambdas keyed by their captures. It's enabled by default since Kotlin 2.0.20+.
- Under Strong Skipping, how are unstable vs stable parameters compared during recomposition?
- Unstable parameters are compared using instance/referential equality (===); stable parameters use structural equality (equals()). So a new instance of an unstable type with equal content still triggers recomposition unless you mark the type @Stable.
- What is a 'deferred state read' and why does it help performance?
- Reading state inside a lambda (e.g. Modifier.offset { IntOffset(x,y) } or graphicsLayer { }) defers the read to the layout/draw phase instead of composition. The frequently-changing state then only invalidates layout/draw, skipping recomposition entirely.
- When should you use derivedStateOf?
- When a frequently-changing state should only trigger recomposition when a derived/computed result actually changes (e.g. showButton = derivedStateOf { listState.firstVisibleItemIndex > 0 }). It avoids recomposing on every scroll pixel and recomputes only when reads change.
- collectAsStateWithLifecycle vs collectAsState?
- collectAsState collects the Flow as long as the composition is active, even when the app is backgrounded. collectAsStateWithLifecycle (from lifecycle-runtime-compose) collects only while the lifecycle is at least STARTED, cancelling in the background to save work and avoid wasted upstream emissions. Prefer it on Android.
- How do you find and debug excessive recomposition?
- Use Layout Inspector recomposition counts to spot hotspots, enable Compose compiler metrics/reports to see which composables are skippable/restartable and which classes are unstable, and measure with Macrobenchmark + Baseline Profiles in a release (R8) build, never debug.