Strong Skipping

Compose Stability, Strong Skipping and Immutable Types

Recomposition Counts watched catalog rows skip recomposition. Compose 234 may skip a composable when all its parameters are equal to last time, and it trusts equals() only for stable types: ones whose public properties are immutable or observable (State), such as primitives, String, function types and data classes made of them. The compiler plugin writes its verdicts to files when asked:

app/build.gradle.kts: Compose compiler reportsGroovy
composeCompiler {                       // 5.36.4 what the compiler decided about stability
    reportsDestination = layout.buildDirectory.dir("compose_compiler")
    metricsDestination = layout.buildDirectory.dir("compose_compiler")
}

The next build wrote app-classes.txt, app-composables.txt and a JSON metrics file; from the first two:

Output of 202
stable class com.example.booknest.data.Book {
  stable val id: Int
  stable val title: String
...
  <runtime stability> = Stable
}
restartable skippable scheme("[androidx.compose.ui.UiComposable]")
    fun com.example.booknest.ui.BookCard(
  book: Book
  stable modifier: Modifier? = @static <expression>
)
restartable skippable scheme("[androidx.compose.ui.UiComposable]")
    fun com.example.booknest.camera.CameraScreen(
  stable modifier: Modifier? = @static <expression>
  unstable vm: CameraViewModel? = @dynamic <expression>
)

Book, all vals of stable types, is inferred stable. CameraViewModel is unstable: it holds CameraX 234 use cases, mutable Java objects the compiler can't reason about. Yet CameraScreen is still skippable, and the metrics file says why: "StrongSkipping": true. Strong skipping, on by default since Kotlin 2.0.20, makes every restartable composable skippable, comparing unstable arguments by instance (===) and stable ones with equals(), and it remembers lambdas automatically. BookNest's release build reported 187 composables, 29 of its 60 classes stable and 21 unstable.

Strong skipping moves the problem rather than removing it: an unstable argument that is a new instance with equal content still forces recomposition. The classic case is a list rebuilt on every emission, such as a Room 234 query that allocates fresh List<Book> objects. The fixes, in order of preference: