Unnecessary recomposition — Compose re-running a function whose output hasn’t actually changed — is the most common source of jank in a Compose app. Here’s how to actually see it happening, rather than guessing.
Method 1: The Layout Inspector
Android Studio’s Layout Inspector (Tools > Layout Inspector, with a debug build running) has a “Recomposition Counts” column you can enable from its settings gear icon. It shows, per composable, how many times it has recomposed and how many times it has skipped recomposing since the app started. A number that climbs continuously while nothing on screen visibly changes is your first clue.
Method 2: A Manual Recomposition Counter
For quick local debugging without switching tools, a small helper that logs on every recomposition is often faster:
@Composable
fun LogRecomposition(tag: String) {
val count = remember { intArrayOf(0) }
count[0]++
Log.d("Recompose", "$tag: ${count[0]}")
}
@Composable
fun ProductCard(product: Product) {
LogRecomposition("ProductCard-${product.id}")
// ...
}
Watch Logcat while interacting with the screen. If ProductCard for every item in a list logs on every scroll frame, something upstream is invalidating state it shouldn’t.
Common Causes, in Order of Likelihood
List<T> or a class with a var property makes Compose treat that parameter as unstable, so it can’t skip recomposition even when the value is identical. See our guide on Compose stability.onClick = { doThing(item) } is a new object each time unless its captured values are stable, which can itself force a child to be treated as changed.A Practical Workflow
Start with the Layout Inspector to find which composable is recomposing too often. Then check its parameters against the stability rules to find why. Fixing stability issues at the data-class level (adding @Immutable, switching a List to a kotlinx.collections.immutable type) usually fixes many composables at once, since instability tends to propagate from a shared model class rather than being unique to one screen.