Recomposition Counts

Tracking Recomposition Counts and Skipped Frames

Layout Inspector's recomposition columns need Studio, but a composable can count itself: remember a counter and bump it in a SideEffect, which runs once after every composition that completes:

ui/Recompositions.kt: a debug-only composition counterKotlin
@Composable
fun LogCompositions(tag: String) {
  if (!BuildConfig.DEBUG) return
  val count = remember { intArrayOf(0) }            // survives recomposition, not a State
  SideEffect {                                       // runs after every successful composition
    count[0]++
    Log.d("BookNestRecompose", "$tag: composition #${count[0]}")
  }
}

With a call in CatalogRoute and in each row (buildConfig = true generates BuildConfig), launching the app and typing "sal" into the search field logged:

Output of 198
D/BookNestRecompose( 2046): CatalogRoute query='': composition #1
D/BookNestRecompose( 2046): row 6: composition #1
...
D/BookNestRecompose( 2046): CatalogRoute query='s': composition #2
D/BookNestRecompose( 2046): CatalogRoute query='sa': composition #3
D/BookNestRecompose( 2046): CatalogRoute query='sal': composition #4

Each keystroke recomposed the screen but no row: unchanged rows were skipped. The Quiet Harbor, row 1, never composed at all, because it sat below the fold and LazyColumn composes only visible items. Strong Skipping explains when skipping fails.

A frame that misses its 16.7 ms deadline (at 60 Hz) is jank. adb shell dumpsys gfxinfo com.example.booknest reports frames since the last reset; here after six flings through the 600-book catalog of Lists with LazyColumn:

Output of 198
Total frames rendered: 34
Janky frames: 33 (97.06%)
50th percentile: 109ms
...
Number Slow UI thread: 30

That indicts the software-rendered emulator on four shared CPUs more than the app. Measure jank on a real phone with a release build, using Scroll Jank's FrameTimingMetric.