Jetpack Compose in Production: Lessons from Real Apps

Jetpack Compose has moved well beyond "early adopter" territory. Production apps at companies of all sizes now ship Compose UIs to millions of users. This article distills the most important lessons learned from real-world deployments — the patterns that work and the pitfalls worth avoiding.

State Management at Scale

The biggest source of bugs in Compose production apps is poorly scoped state. The pattern that works best at scale is Unidirectional Data Flow (UDF): a single screen state object flows down from the ViewModel, and events flow up as sealed classes or lambdas.

data class HomeUiState(
  val items: List<Item> = emptyList(),
  val isLoading: Boolean = false,
  val errorMessage: String? = null
)

This keeps composables stateless (easy to preview and test) while the ViewModel owns all business logic.

Performance: Recomposition Is the Enemy

Unnecessary recomposition is the number-one performance issue in Compose apps. The most effective mitigations:

  • Use @Stable and @Immutable annotations on your UI state classes so the compiler can skip recomposition.
  • Prefer LazyColumn keys to stable IDs — without them, list items recompose on every data change.
  • Use the Layout Inspector's recomposition highlights to find hot spots before optimizing.
  • Avoid reading state inside lambdas that trigger often — read it at the call site once.

Interoperability with View System

Most production migrations are incremental. The two bridges you will use constantly:

  • ComposeView — embed a Compose UI inside an existing Fragment or Activity layout.
  • AndroidView — use a traditional View (e.g., a custom chart, MapView, or legacy component) inside Compose.

Both work reliably, but watch out for lifecycle mismatches. Use rememberUpdatedState and DisposableEffect to handle cleanup correctly when using AndroidView.

Testing Compose UIs

Compose's test APIs are excellent. The key tools:

  • createComposeRule() for unit-level UI tests without a device.
  • Semantic matchers (hasText(), hasContentDescription()) are more resilient than pixel-based assertions.
  • Use composeTestRule.awaitIdle() after state changes to prevent flaky tests.

Key Takeaway

Compose production success comes down to discipline around state, not Compose itself. Keep state hoisted, keep composables dumb, and profile before optimizing. The framework rewards clean architecture.