100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

Common Android & Compose Pitfalls

The recurring mistakes developers make with recomposition, state, coroutines, and lists in Jetpack Compose, and the concrete fixes for each.

Interview PrepIntermediate10 min readJul 8, 2026
Analogies

Common Android & Compose Pitfalls

Compose's declarative model removes a lot of the boilerplate and manual bookkeeping of the View system, but it introduces its own set of subtle traps for developers coming from imperative UI toolkits, or even from earlier Compose code that predates best practices. Most of these pitfalls share a common root: misunderstanding when recomposition happens, what survives it, and what scope a piece of state or a coroutine actually lives in. This topic walks through the mistakes that show up most often in real codebases and code review, with the reasoning for why each one is wrong and what to do instead.

🏏

Cricket analogy: Just as a captain who doesn't understand DRS reviews keeps burning them at the wrong moments, a developer who doesn't understand recomposition scope keeps triggering rebuilds at the wrong moments in the UI tree.

Unstable Lambdas and Unnecessary Recomposition

A very common performance pitfall is creating a new lambda instance on every recomposition of a parent, then passing it to a child composable, which can defeat Compose's skipping optimization if the child's other parameters are otherwise stable — Compose sees a 'new' lambda reference each time and may treat the child as needing recomposition. The usual fix is to hoist the lambda reference (e.g. a ViewModel method reference like viewModel::onItemClick) so the same instance is reused, or to wrap ad-hoc lambdas in remember when they close over values that change infrequently. This matters more in hot paths like LazyColumn items than in one-off top-level screens.

🏏

Cricket analogy: It's like a bowler resetting their run-up mark before every single delivery instead of reusing the same fixed mark — the extra unnecessary setup work slows the over down, just as a fresh lambda each recomposition defeats skipping.

Reading State Too High in the Tree

Another frequent mistake is reading a frequently changing State value (like a scroll offset or an animated value) at a high level in the composable tree, near the root, instead of down inside the specific composable that actually needs it. Because recomposition scope is determined by where a State is read, reading it high up forces a large portion of the tree to recompose on every change, even though only one small leaf composable actually uses the value. Pushing the read down — sometimes via a lambda-based modifier like Modifier.graphicsLayer { ... } which reads state during the layout/draw phase rather than composition — confines the recomposition (or avoids it altogether) to the smallest possible scope.

🏏

Cricket analogy: Reading the pitch report at the team hotel instead of walking out to check it at the crease means the whole squad reacts to information nobody local actually needed, like reading fast-changing state high in the tree instead of down at the leaf that uses it.

kotlin
// Pitfall: reading scroll state high up forces the whole screen to recompose on every scroll tick.
@Composable
fun ProfileScreen(listState: LazyListState) {
    val offset = listState.firstVisibleItemScrollOffset // read here -> big recomposition scope
    Column {
        Header(offset)
        ProfileList(listState)
    }
}

// Fix: defer the read into the draw phase via graphicsLayer, avoiding composition-level recomposition.
@Composable
fun ProfileScreen(listState: LazyListState) {
    Column {
        Header(
            modifier = Modifier.graphicsLayer {
                translationY = -listState.firstVisibleItemScrollOffset.toFloat() * 0.5f
            }
        )
        ProfileList(listState)
    }
}

The Layout Inspector's recomposition counts (or the 'Recomposition Highlighter' in newer Android Studio versions) are the single most effective diagnostic tool for these issues — rather than guessing, enable them and watch which composables light up on an interaction that shouldn't affect them, then trace the State read causing it.

Launching a coroutine directly with GlobalScope.launch inside a composable or ViewModel is a classic pitfall: it is not tied to any lifecycle, so it keeps running (and can leak) even after the screen or ViewModel is gone. Use viewModelScope in a ViewModel, or LaunchedEffect / rememberCoroutineScope inside a composable, so the coroutine is automatically cancelled when its owning scope ends.

A related, subtler pitfall: declaring var someValue = mutableStateOf(...) as a plain property inside a composable function body without wrapping it in remember { } is a subtle but serious bug: on every recomposition, the composable function re-executes from the top, creating a brand-new MutableState instance and silently discarding the previous one, so any user interaction that updated the value gets reset the moment something else triggers recomposition. The fix is always to wrap state creation in remember (or rememberSaveable when persistence across configuration change is needed) so the same instance is reused across recompositions of that call site.

🏏

Cricket analogy: It's like a scorer who re-opens a brand-new blank scorebook every over instead of continuing the same book — every wicket and run tally recorded earlier is silently wiped, just as an un-remembered mutableStateOf resets on every recomposition.

  • Unstable lambdas passed to children can defeat Compose's skipping optimization; hoist or remember them in hot paths.
  • Reading frequently changing state high in the tree widens the recomposition scope unnecessarily; push reads down, or defer to draw phase via graphicsLayer.
  • Use Layout Inspector's recomposition counts to diagnose unexpected recomposition rather than guessing.
  • Never use GlobalScope for coroutines tied to UI or ViewModel logic; use viewModelScope or LaunchedEffect/rememberCoroutineScope instead.
  • Forgetting to wrap mutableStateOf(...) in remember silently resets state on every recomposition.
  • Most Compose pitfalls trace back to misunderstanding recomposition scope or coroutine/state lifetime.

Practice what you learned

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#CommonAndroidComposePitfalls#Common#Android#Compose#Pitfalls#StudyNotes#SkillVeris