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
@Stableand@Immutableannotations on your UI state classes so the compiler can skip recomposition. - Prefer
LazyColumnkeys 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.