company logo
Blog Banner Bg

Migrating a Legacy Android App from XML Views to Jetpack Compose

A practical, incremental plan for moving a large production Android codebase from XML layouts to Jetpack Compose — interop, what breaks, and realistic timelines.

Aug 5, 202612 min read

Migrating a Legacy Android App from XML Views to Jetpack Compose

Every Android app built before 2021 has the same layer at its foundation: XML layouts, findViewById or View Binding, adapters, and a View hierarchy that has accumulated five or more years of fixes. Jetpack Compose is now where Google puts new APIs first, where the hiring market for Android engineers has shifted, and where new UI work happens on most active projects. The gap between "the toolkit our app is built on" and "the toolkit the platform is moving toward" is the reason this question keeps coming up in planning meetings.

The good news is that a full rewrite is almost never the right way to close that gap. Compose was designed to run inside an existing View-based app, screen by screen, for as long as that takes. The bad news is that "it's interoperable" gets oversold — interop handles coexistence, not the parts of the app that are entangled with the View system in ways that don't map cleanly onto Compose's model.

This is a plan for CTOs and engineering managers sitting on a large legacy Android codebase who need to scope this honestly: how the incremental path actually works, where interop holds up and where it doesn't, what tends to break, and how long it realistically takes.

What "migrating to Compose" means for a shipping app

It does not mean stopping feature work for a quarter to rebuild the UI. It means running two UI toolkits in the same app, in the same Activity, sometimes on the same screen, for the length of the migration — which for a real app is measured in quarters, not weeks — while the app keeps shipping releases.

Why an incremental migration, not a rewrite

A full rewrite asks a business to freeze the UI layer while a team re-implements screens it already has working code for, with no revenue upside until the last screen ships. For a production app with real users, that is a hard sell to a board, and worse, it re-introduces every UI bug the current version has already had fixed.

The incremental path treats Compose as an additive capability rather than a replacement project:

  • New screens and new features get built in Compose from day one, so the codebase stops growing its XML surface
  • Existing screens get migrated opportunistically — when a screen needs rework anyway, it is rebuilt in Compose instead of patched in XML
  • The two toolkits coexist in the same app for the entire migration window, which can be a year or more for a large app
  • Nothing ships that has not been tested — a half-migrated screen is still a real, releasable screen, not a work-in-progress branch

The tradeoff is real: you carry two toolkits, two mental models, and (for a while) two sets of UI tests. That cost is the price of not stopping the business. For a codebase with dozens of screens and an active roadmap, it is almost always the cheaper option.

How the interop layer actually works

Compose's interop APIs are the mechanism that makes incremental migration possible, and they run in both directions.

Compose inside an existing View screen. A ComposeView is a regular android.view.View subclass. You can drop one into an existing XML layout, or add it programmatically to a ViewGroup, and set its content with setContent { }. This is usually how migration starts — a single new component (a card, a form field, a bottom sheet) gets built in Compose and embedded in a screen that is otherwise still XML. ComposeView needs an explicit ViewCompositionStrategy, most commonly disposing composition when the view's lifecycle owner is destroyed; getting this wrong is a common source of the "Compose content update leaks after navigating away" class of bug.

Views inside Compose. The AndroidView composable does the reverse — it hosts a View or ViewGroup inside a Compose tree, with factory and update callbacks for creation and recomposition. This is what lets a fully Compose screen still embed a MapView, a WebView, or a third-party SDK's View that has no Compose equivalent, without waiting for that vendor to ship one.

Fragments and Navigation. If the app uses the Navigation component with Fragment destinations, AndroidViewBinding and FragmentContainerView let a Fragment-based destination sit inside a Compose-based navigation graph, and vice versa via ComposeView inside a Fragment's onCreateView. This works, but it is the part of the interop story that most often needs a design decision rather than a mechanical swap — see below.

Where interop is genuinely solid: simple content composition, embedding one toolkit's leaf components inside the other's container, and gradual per-screen replacement. Google has run this exact model on its own large apps, and the APIs have been stable since Compose 1.0.

Where it needs real engineering, not just wiring: shared element and custom transitions between a Fragment and a Compose destination, deep two-way data binding between an old ViewModel pattern and Compose state, and any custom View that does its own measurement or drawing logic that Compose's layout system doesn't know about.

android studio code editor showing layout markup

What actually breaks

This is the part briefs and vendor pitches tend to skip. None of the following are blockers, but each one is a real piece of work, and underestimating them is where migration timelines go wrong.

Custom Views with their own drawing or measurement logic

A View subclass that overrides onDraw, onMeasure, or onLayout — a custom chart, a gauge, a signature pad — has no Compose equivalent to drop in. It gets wrapped via AndroidView (fine, ships immediately) or rewritten against Compose's Canvas and layout APIs (more work, but gets you off the legacy View lifecycle entirely). Rewriting is usually worth it only for views the team expects to keep changing.

MotionLayout and complex XML-driven animation

Screens built around MotionLayout scenes, especially ones with several interpolated states and gesture-driven transitions, do not have a drop-in Compose replacement. Compose's animation APIs (animateFloatAsState, Transition, gesture detectors) are more composable but require the animation logic to be rebuilt, not converted mechanically. Treat any screen with heavy MotionLayout choreography as a redesign task, not a port.

RecyclerView adapters and DiffUtil logic

RecyclerView plus a ListAdapter and DiffUtil becomes LazyColumn or LazyRow plus a Kotlin list and key parameters — conceptually simpler, but every adapter's view-type switching, item decoration, and scroll-position-restoration logic has to be re-expressed in Compose's model. For screens with heterogeneous item types or complex item decorations, this is one of the larger single line items in a migration estimate.

Themes, styles, and design tokens

XML themes and style inheritance (parent="..." chains, attribute resolution) do not map one-to-one onto Compose's MaterialTheme and token-based approach. A design system built as XML styles over several years has to be re-expressed as a Compose theme — colors, typography, and shapes as Kotlin objects — before screen migration can proceed confidently. Skipping this step is why early-migrated screens in some codebases visually drift from the rest of the app.

State management and architecture debt

Compose's unidirectional data flow (state flows down, events flow up) surfaces every place the existing MVP or MVVM implementation relied on the View holding mutable state, a Fragment lifecycle callback doing work it shouldn't, or a Presenter reaching directly into a View reference. Migrating the UI layer without touching architecture is possible for isolated screens, but for anything with shared state across screens, some ViewModel and state-holder rework typically comes along for the ride whether it was scoped or not.

Third-party SDKs and ad/analytics views

Ad SDKs, map SDKs, and some analytics or payment SDKs still ship View-based integration points. AndroidView handles hosting them, but SDK versions vary in how well they behave when wrapped — recomposition-triggered re-creation of the wrapped View is a real failure mode worth testing early, not assuming away.

Testing

Espresso's ViewMatchers and Compose's semantics-based ComposeTestRule are different testing models. A screen that mixes both toolkits needs both test approaches during the transition, and UI test suites tend to need real rework, not a search-and-replace, as screens move over.

A realistic migration order

Most successful migrations follow the same rough sequence, not because it is dogma but because it minimizes the number of places two toolkits have to talk to each other at once.

  1. Foundations first. Stand up the Compose theme (colors, typography, shapes) mapped from the existing design system, add the Compose BOM and required Kotlin/AGP versions, and get one trivial screen rendering to prove the build and dependency setup end to end.
  2. New screens in Compose, always. From the day the foundation lands, every new screen is Compose. This is the highest-leverage rule in the whole plan — it caps how much XML the team can add.
  3. Leaf screens next. Migrate screens with few dependents and simple navigation in and out — settings pages, detail screens, standalone forms. These validate the interop pattern with low blast radius if something is wrong.
  4. Shared components, once, centrally. Buttons, cards, list items, and form fields used across many screens get built once in Compose and adopted everywhere, rather than rebuilt per screen.
  5. High-traffic and stateful screens last. Home feeds, checkout flows, anything with complex shared state or heavy custom animation — these carry the architecture debt described above and benefit from the team having several migrations of practice before touching them.
  6. Navigation graph consolidation. Once most destinations are Compose, migrate the Navigation graph itself to Compose Navigation, retiring the Fragment/Compose bridging layer rather than carrying it indefinitely.

How long it actually takes

There is no honest single number here — it depends on screen count, how tangled the architecture is, and how much of the team has done this before. What can be said with more confidence is the shape of the estimate:

FactorEffect on timeline
App size (screen count)The dominant variable — a 30-screen app and a 300-screen app are different projects, not different durations of the same project
Custom Views and animation-heavy screensEach one is closer to a redesign than a port; budget accordingly rather than averaging it into a per-screen rate
Architecture pattern already in placeA clean MVVM with a real state layer migrates faster than a codebase with View logic scattered across Activities and Presenters
Team's Compose experienceA team doing this for the first time should expect the first few screens to take longer than the fiftieth
Release cadence during migrationMigrating while still shipping features on a normal cadence is slower per-screen but avoids a roadmap freeze

As a planning anchor, not a quote: a simple, low-interaction screen (a settings page, a static detail view) with the foundations already in place typically runs one to two developer-weeks including tests. A screen with custom animation, complex list behavior, or entangled state can run several times that. For a mid-sized app (50–100 screens) with a team of two to four engineers who have some Compose experience, a realistic full migration — including the shared-component and navigation consolidation work — runs somewhere in the range of 12 to 24 months alongside normal feature delivery. Larger legacy codebases, or ones with heavy custom View and animation work, run longer. Any estimate given before a codebase audit has actually inventoried the screen count and flagged the animation-heavy and custom-View screens is a guess, and should be labeled as one.

Team readiness and process changes

The technical migration is usually not the part that stalls projects — team habits are.

  • Code review needs two rubrics for a while. Reviewers need to know both the XML/View conventions and Compose conventions, and PRs mixing both toolkits need reviewers comfortable with the seam between them.
  • State management conventions should be written down early, not discovered screen by screen. Without an agreed pattern for how Compose state, ViewModels, and any existing Presenter layer interact, different engineers will invent different answers and the codebase fragments further.
  • QA and automated test coverage need a transition plan. Screens under active migration are the highest-risk regression surface in the app; visual regression testing and a stable Compose test setup earn their cost here.
  • Ramp-up time is real and worth budgeting, not treating as a rounding error. Engineers fluent in the View system are not automatically fluent in Compose's state and recomposition model, and the first few screens are where that gap shows up as bugs, not just slower delivery.

How CodeDTX approaches an XML-to-Compose migration

CodeDTX works with engineering teams around the migration itself — codebase assessment, the interop and architecture setup, screen-by-screen implementation, and the QA needed to ship each migrated screen with confidence.

Codebase audit and migration plan

We inventory screens, custom Views, animation-heavy flows, and the current architecture pattern, and flag the parts of the app that will not migrate mechanically before a single line of Compose is written. That inventory is what turns "how long will this take" into a real estimate instead of an average.

Foundation and interop setup

Standing up the Compose theme against the existing design system, wiring the Compose BOM and build configuration, and establishing the ComposeView/AndroidView interop pattern the rest of the team will reuse for the length of the migration.

Screen-by-screen implementation

Migrating screens in the order that minimizes risk — leaf screens and shared components first, stateful and animation-heavy screens once the pattern is proven — while new feature work ships in Compose from day one.

Architecture and state layer work

Where a screen's existing MVP or MVVM implementation does not map cleanly onto unidirectional state flow, resolving that as part of the migration rather than leaving a Compose UI bolted onto a View-era state model.

QA across both toolkits

Regression testing on the mixed-toolkit app throughout the transition, and building out Compose-based UI test coverage as screens move over, so migrated screens are verified rather than assumed correct.

Team enablement

Pairing and review support to bring an existing Android team up to speed on Compose conventions, so the migration builds internal capability rather than creating a codebase only the migration team understands.

Responsibility split

AreaOwner
Product priorities and release cadenceClient
Design system source of truthClient
Codebase audit and migration sequencingJoint
Compose foundation and interop setupCodeDTX
Screen-by-screen migration implementationCodeDTX
Architecture and state layer reworkJoint
QA and regression testingCodeDTX
Team training and code review supportJoint
App Store releases and rolloutClient

Questions to answer before you scope this

  • How many distinct screens does the app actually have, and how many rely on custom Views or MotionLayout?
  • What architecture pattern is in place today — and how much of it assumes a View holding state directly?
  • Which third-party SDKs ship View-based UI, and are current versions known to behave inside AndroidView?
  • Is the design system already tokenized, or does it live only as XML styles and resource files?
  • What is the team's current Compose experience, honestly — has anyone shipped a production Compose screen before?
  • Can the release cadence absorb screens shipping in mixed states for the length of the migration?
  • Is there a navigation library in place today, and is migrating it in scope for this phase or a later one?

If most of these are open questions right now, that is normal — a short codebase audit is how they get answered, and it is worth doing before committing to a timeline.

Move off XML on your own schedule

Migrating to Compose is a real engineering programme with a real cost, but it does not require stopping the roadmap to run it. The incremental path — new screens in Compose immediately, existing screens migrated in an order that limits risk, interop carrying the seam in between — lets a legacy Android app move forward without a rewrite-sized bet.

If you are scoping this for a codebase you did not build from scratch, get in touch. We will start with an audit of what is actually in the app — screen count, custom Views, architecture pattern — and give you a sequencing and timeline that reflects your codebase, not a generic estimate.

ellipse
bg gradient

Let’s work together

We are here to help you make your big idea a reality!