Stability and skipping

Jetpack Compose Senior Kotlin 2.0.20+ API 23+ Reviewed By Best Latest Apps

Predict the output

Strong skipping is on. The parent recomposes because query changed. Does TagRow recompose?

@Composable
fun Parent(query: String, tags: List<String>) {
    Text(query)
    TagRow(tags = tags.filter { it.isNotBlank() })
}

@Composable
fun TagRow(tags: List<String>) { Text(tags.joinToString()) }
Options
Hint 1 of 2

Is filter's result the same object as last time?

Hint 2 of 2

How does strong skipping compare unstable parameters?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Yes: filter returns a new list, and unstable parameters are compared by instance

Correct. A new List instance every run fails the === check, so TagRow recomposes.

Why not the others?
  • B No: the list contents are the same, so it's skipped. With an unstable type, strong skipping compares instances, not contents.
  • C No: lists are always stable in Kotlin 2. List is still treated as unstable without a stability config.
  • D Yes, but only because TagRow has no key. Keys are for lists of items, not for skipping.
Indented list: Parent with Text and TagRow. When query changes, compare filtering each run with remembering the filtered list.

Illustrative, based on Compose and Android rules; not a device recording

Text version
Recompositions after: query changes once
Composablefilter each runremember(tags) { filter }
Parent1 recomposition1 recomposition
 Text(query)1 recomposition1 recomposition
 TagRow1 recomposition0: skipped

30-second answer

Compose can skip a composable when its parameters are equal to last time. A type is stable if Compose can trust that equality: primitives, Strings, lambdas, and classes whose public properties are all val of stable types. With strong skipping, which is on by default with the Kotlin 2 Compose compiler, unstable parameters are compared by instance (===), so a new List each time still forces recomposition.

Practise saying it · 45 s. No recording, just a timer

Explain stability and skipping.

Say your answer out loud, then check which of these points you covered:

  • Compose skips when parameters are equal
  • Stable types can be compared with equals
  • Strong skipping compares unstable params by instance
  • Avoid new lists per run; use remember or immutable collections
  • Measure first

Deep dive

List<T> is an interface, so the compiler treats it as unstable; kotlinx.collections.immutable types or a stability configuration file fix that.

@Immutable promises nothing ever changes; @Stable promises changes are reported through Compose state. Lying to the compiler causes stale UI.

Classes from modules not compiled with the Compose compiler are unstable unless listed in a stability config.

The Compose compiler reports (composeCompiler { reportsDestination = ... }) list each class's stability.

Remembering derived data Compile-checked · Kotlin 2.4.21
@Composable
fun ParentFixed(query: String, tags: List<String>) {
    val visible = remember(tags) { tags.filter { it.isNotBlank() } }
    Text(query)
    TagRowFixed(tags = visible)
}

@Composable
fun TagRowFixed(tags: List<String>) { Text(tags.joinToString()) }

Common mistakes

  • Building a new list or lambda capturing changing values on every recomposition and wondering why children recompose.
  • Adding @Immutable to a class with var properties.
  • Optimising stability without measuring recomposition counts first.

Senior take & follow-ups

Seniors mention strong skipping as the current default and say they measure first with Layout Inspector or compiler reports.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does strong skipping change for lambdas?

    Show hint

    Lambdas are remembered automatically based on their captures.

  2. How do you check stability?

    Show hint

    Enable the Compose compiler reports and read the classes file.

Suggest a fix if something here is wrong or out of date.