Jetpack Compose has been production-ready for a few years now, but the architecture question — how to structure a large app — remains one of the most debated topics in the Android community. This post shares what's worked for us across multiple apps.
The Core Principle: Unidirectional Data Flow
Every Compose architecture debate eventually converges on the same principle: state flows down, events flow up. The practical implementation is a ViewModel that exposes a single StateFlow and accepts events via sealed class hierarchies.
The composable never knows where its data comes from. It renders what it receives and dispatches events upward.
Screen-Level vs Component-Level ViewModels
A common mistake is creating a ViewModel for every composable. In practice, we use screen-level ViewModels and pass state down as plain data classes. Only reach for a component-level ViewModel when a component has genuinely independent lifecycle requirements.
Navigation
We use the Navigation Compose library with a type-safe wrapper. Define your routes as sealed objects, not raw strings — string-based navigation is a maintenance hazard in a large app.
Conclusion
The patterns aren't revolutionary. They're an application of solid software design principles to the Compose lifecycle. Start with a screen-level ViewModel, a single UiState class, and a sealed events class — then refactor when complexity demands it.