Begin with a migration reason, not an adoption target
Compose can make state-driven UI and reusable components easier to express. That is not enough reason to rewrite stable screens. Name the problem first: duplicated UI logic, slow feature work, an upcoming redesign, accessibility debt, or difficulty sharing components across a growing product.
The migration succeeds when it improves that constraint without destabilizing releases. “Percentage of screens in Compose” is not a product outcome.
The incremental migration approach
Google's current migration guidance recommends incremental adoption while Compose and Views coexist. A practical sequence is:
- Phase 1: Adopt Compose for all new screens and features. Leave existing screens untouched.
- Phase 2: Migrate isolated components within existing screens. Start with leaf-level widgets like buttons, cards, and input fields.
- Phase 3: Convert full screens to Compose when major refactors or redesigns are already planned.
This phased approach keeps your release cadence intact. No team goes dark for months on a rewrite. Every sprint still ships value to users.
ComposeView in existing activities and fragments
The bridge between the old and new worlds is ComposeView. You can drop a ComposeView into any XML layout or add it programmatically to a ViewGroup. Inside it, you write pure Compose code.
In practice, this means you can replace a single RecyclerView item layout with a Compose component while the rest of the screen stays in XML. The parent Fragment does not need to change. The ViewModel does not need to change. Navigation stays the same.
Going the other direction works too. The AndroidView composable lets you embed traditional Views inside Compose screens. This is critical for components like MapView or complex custom Views that are not worth rewriting.
State management for enterprise: StateFlow + ViewModel
Compose needs observable state with clear ownership. StateFlow exposed from a ViewModel is one good option, especially when the existing architecture already uses coroutines. Compose can collect it with collectAsStateWithLifecycle().
This pattern works well for several reasons:
- ViewModels survive configuration changes without extra ceremony.
- StateFlow provides a single source of truth that both Compose and legacy View code can observe.
- Unidirectional data flow makes debugging straightforward. State goes down, events go up.
- The same ViewModel can serve both a Compose screen and a View-based screen during migration. No duplication needed.
Keep state with the narrowest owner that must survive and coordinate it. A ViewModel is appropriate for screen-level state, but not every toggle, expansion state, or reusable component value belongs there.
Performance considerations
Compose is fast, but careless code will create jank. Recomposition is the main thing to watch. Every time state changes, Compose re-executes the affected composable functions. If your composable reads more state than it needs, it recomposes too often.
Key practices we follow:
- Use lazy containers when the collection is large or unbounded. For a small fixed group, a regular layout may be simpler and faster to reason about.
- Provide stable keys to lazy list items. Without them, Compose cannot efficiently diff your list on updates.
- Defer state reads as deep into the composition tree as possible. Pass lambdas instead of raw state values to child composables.
- Use the Layout Inspector in Android Studio to identify unnecessary recompositions. The recomposition counter is your best friend during optimization.
Do not optimize from folklore. Capture a baseline, use release builds, test on a representative lower-end device, and profile the interaction that matters.
Testing Compose UI
Compose provides semantics-based UI testing APIs for locating nodes, performing actions, and asserting state. Tests still become flaky when clocks, asynchronous work, navigation, or external services are uncontrolled. Compose changes the tools, not the need to design test seams.
Use plain unit tests for reducers and business rules, Compose UI tests for meaningful component and screen behavior, and a small number of end-to-end tests for critical journeys. Screenshot testing can help with visual regressions, but it needs deliberate device, font, locale, and tolerance controls.
When not to migrate
Not every screen deserves a migration. Some modules should stay exactly as they are.
- Legacy screens that are stable, rarely changed, and have comprehensive test coverage. The risk of introducing bugs outweighs any productivity gain.
- Screens with deeply custom View implementations like canvas-based drawing, complex touch handling, or hardware-accelerated rendering paths.
- Modules scheduled for deprecation. If a feature is being removed in six months, do not invest in migrating it.
Pragmatism beats purity. The goal is a better codebase that ships reliable software, not a Compose adoption percentage on a dashboard.
Define the first safe boundary
Choose one screen with active product work, manageable dependencies, and observable performance. Ship it through the existing release path. Record what changed in build time, test stability, accessibility, and development effort before choosing the next boundary.
If your team is planning a migration, contact DEVSFLOW. We can review the current architecture and identify a bounded first stage that preserves the existing release path.
Need a senior Android engineer who knows Jetpack Compose inside and out? Our engineers can embed into your team and help you migrate without disrupting your release cadence. Learn about staff augmentation