Understanding Recomposition in Jetpack Compose

What is Recomposition?

Recomposition is the process by which Jetpack Compose updates the UI in response to state changes. Compose observes state reads within a composable and re-executes that function when the state changes. Ideally, recompositions are scoped and efficient—but unintentional recompositions can happen due to:

  • State hoisting misalignment
  • Mutable state used incorrectly
  • Passing lambdas or data without remember or key
  • Recomposable functions declared in-place

Symptoms of Recomposition Loops

  • Excessive CPU usage during idle UI state
  • Laggy scroll or animation jank
  • Profiler shows repeated recomposition of the same nodes

Architectural Triggers of the Problem

Mismanaged State Ownership

Improperly scoped state—such as ViewModel state directly mutated in a composable—causes excessive recompositions. State should ideally be owned by the ViewModel and exposed immutably via StateFlow or LiveData, and observed with collectAsState().

Function Reference Recreation

In-place lambdas or object creation inside composables lead to new instances on every recomposition, triggering child recompositions.

@Composable
fun ParentComposable() {
    ChildComposable(onClick = { println("Clicked") }) // Anti-pattern
}

Instead, use:

@Composable
fun ParentComposable() {
    val onClick = remember { { println("Clicked") } }
    ChildComposable(onClick = onClick)
}

Diagnostics and Tools

Using Layout Inspector and Compose Metrics

Android Studio's Layout Inspector and "Composition Tracing" tools highlight recomposition count. You can also enable metrics:

build.gradle (app)
android {
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.0"
    }
}

android {
    kotlinOptions {
        freeCompilerArgs += "-P"
        freeCompilerArgs += "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=build/compose-metrics"
    }
}

Using Recompose Highlighter

Jetpack Compose offers a visual debug tool:

Modifier.recomposeHighlighter()

This helps visually detect which parts of the UI are recomposing.

Step-by-Step Fix Strategy

1. Hoist State to ViewModel

Move stateful logic from composables into the ViewModel:

val uiState by viewModel.stateFlow.collectAsState()

2. Use 'remember' for Lambdas and Constants

@Composable
fun MyScreen() {
    val clickHandler = remember { { /* handle click */ } }
    Button(onClick = clickHandler) { Text("Click") }
}

3. Avoid In-line Object Allocations

Move allocations like Modifier chains, lists, or objects outside of the composable scope or wrap in remember.

4. Use 'key' to Isolate Recompositions

LazyColumn {
    itemsIndexed(items, key = { _, item -> item.id }) { _, item ->
        MyItem(item)
    }
}

5. Profile Before and After

Always use Android Studio Profiler and Composition Tracing to validate improvements.

Best Practices

  • Always hoist state as high as feasible
  • Use remember and derivedStateOf for derived values
  • Profile on real hardware to detect rendering jank
  • Use stable data models (avoid mutable maps/lists)
  • Defer heavy work outside of composables (e.g., in ViewModel or coroutines)

Conclusion

Jetpack Compose's declarative nature simplifies UI creation, but demands precision in managing state and recomposition. Unintended recomposition loops are often a symptom of state mismanagement, excessive in-place declarations, or misuse of composable scope. By following a structured approach to diagnose and correct these patterns, teams can preserve UI performance, battery efficiency, and maintainability—ensuring that Compose delivers on its promise of modern, elegant Android development.

FAQs

1. How do I know if recomposition is a problem in my Compose app?

Use the Layout Inspector and recomposition highlighter tools to check for frequent re-renders of unchanged UI.

2. Is it bad to declare lambdas inside composables?

Yes, if not wrapped in remember. They recreate on each recomposition, triggering unnecessary updates.

3. Does using StateFlow over LiveData help performance?

Yes. StateFlow integrates natively with Compose and provides better lifecycle handling and immutability for recomposition.

4. Can Modifier chains cause recompositions?

Yes, if created inline without remember. Allocate them once and reuse where possible.

5. Should I avoid nested composables?

Not necessarily. Just ensure each composable has a clear boundary and doesn't hold unintended state or side effects.