Why Incremental, Not Big Bang
The most common Compose migration mistake is attempting a full rewrite. In my experience leading 5 production migrations, the incremental approach is the only one that works for apps with real users and active development teams. A big-bang rewrite means feature freeze, regression risk, and a painful merge conflict period that can stretch for months.
The incremental approach lets you ship Compose code to production within the first week, gain team experience gradually, and maintain velocity on feature work throughout the migration.
Phase 1: New Screens in Compose
The lowest-risk starting point is building all new screens in Compose while leaving existing screens untouched. This requires setting up Compose dependencies, configuring the theme to match your existing design system, and establishing a pattern for Compose screens to coexist with XML-based Activities and Fragments.
I use a ComposeActivity base class that provides the theme wrapper, error boundary, and analytics hooks that every screen needs. New features are built entirely in Compose from day one, giving the team hands-on experience without risking existing functionality.
Phase 2: Island Migration
Once the team is comfortable with Compose fundamentals, start replacing individual UI components within existing XML screens. ComposeView lets you embed Compose content inside any XML layout. Target high-churn components first — UI elements that change frequently are the ones that benefit most from Compose's declarative model and will generate the most time savings.
Common candidates for island migration:
- Bottom sheets and dialogs — self-contained UI with clear state boundaries
- Settings screens — form-heavy UIs where Compose's state management shines
- List item layouts — complex RecyclerView items can be replaced with Compose items using
ComposeViewin the ViewHolder - Custom views — complex Canvas-based custom views are often cleaner as Compose Canvas implementations
Phase 3: Navigation Bridge
The trickiest part of incremental migration is navigation. Your app likely uses the Fragment-based Navigation Component, and Compose has its own NavHost. Running both simultaneously requires a bridge.
My approach: keep the Fragment Navigation Component as the top-level router. Each Fragment either contains XML layout or a full-screen ComposeView. This avoids the complexity of running two navigation systems simultaneously and preserves deep link handling, back stack behavior, and transition animations.
Eventually, when enough screens are Compose-only, you migrate the navigation itself — but this should be the last step, not the first.
State Management Across the Boundary
The biggest technical challenge is sharing state between Compose and XML parts of the screen. ViewModels with StateFlow provide the cleanest bridge — both XML views (via lifecycleScope.launch { flow.collect {} }) and Compose (via collectAsStateWithLifecycle()) can observe the same state source.
Avoid creating separate state management for Compose and XML portions of the same screen. One ViewModel, one source of truth, multiple observers.
Lessons from 5 Migrations
- Theme parity takes longer than you expect. Map every color, typography style, and shape from your XML theme to Compose MaterialTheme before writing any UI code
- Preview functions are Compose's killer feature for migration velocity. Invest in comprehensive previews from day one
- Do not migrate screens that are stable and rarely touched. The ROI is negative for code that nobody changes
- Compose increases APK size initially (by 2-4MB for the runtime). This stabilizes and eventually decreases as you remove XML layouts and view binding code
- Team training is the bottleneck, not technical complexity. Budget time for the mental model shift from imperative to declarative UI