Stability and Skipping

Stability, Skipping and Avoiding Unnecessary Recomposition

A child is skipped when its arguments are unchanged. Stable types (primitives, String, lambdas, classes of stable vals) are compared with equals(); unstable ones, such as a class with a var, are compared by instance under strong skipping (the default since Kotlin 2.0.20). On each of five taps StabilityDemo builds a new, equal Shelf("Staff picks", books) for ShelfBanner, and the same data as an @Immutable PinnedShelf for PinnedBanner:

Output of 88
D/BookNestState: StabilityDemo composed #1
D/BookNestState: ShelfBanner composed #1
D/BookNestState: PinnedBanner composed #1
D/BookNestState: StabilityDemo composed #2
...
D/BookNestState: StabilityDemo composed #6

Both banners were skipped every time. With composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") } in app/build.gradle.kts, after ./gradlew clean:

Reading the Compose compiler's stability reportShell
./gradlew compileReleaseKotlin
grep -A4 "demo.Shelf\|demo.PinnedShelf" app/build/compose_compiler/app-classes.txt
Output
runtime class com.example.booknest.demo.Shelf {
  stable val name: String
  runtime val books: List<Book>
  <runtime stability> = Uncertain(List)
}
stable class com.example.booknest.demo.PinnedShelf {
  stable val name: String
  runtime val books: List<Book>
}

Shelf is uncertain, not unstable, because List is an interface; the generated call passes no "unstable" bit in $changed, so ShelfBanner compared with equals() and skipped. So measure (these logs, or Layout Inspector's counts) rather than guess, and fix real cases by creating the object once, using ImmutableList from kotlinx.collections.immutable (https://github.com/Kotlin/kotlinx.collections.immutable 1,421 ), or an honest @Immutable.