← All posts

Why I migrated to Jetpack Compose from XML views

May 13, 2026 5 min read

I put off learning Jetpack Compose for eight months. My excuse was production stability. The real reason was that I didn’t want to unlearn six years of XML muscle memory.

That’s the honest version. Here’s what I actually found once I committed.

The mental model shift

XML layouts describe structure. You define what exists, then wire it to data imperatively in code — binding.userName.text = user.name. You manage the relationship between data and UI yourself.

Compose describes state. You define a function that takes state and returns UI. When state changes, Compose figures out what needs to redraw. You stop managing the relationship. You just describe what the UI should look like for a given state.

  XML + ViewBinding              Compose
  -----------------              -------
     state changes                  state changes
          |                              |
          v                              v
   you write the update           recomposition runs
   binding.name.text = ...        Greeting(state)
          |                              |
          v                              v
   view tree mutated              UI redrawn from state
          |                              |
          v                              v
   UI is correct IF every         UI is correct BECAUSE
   code path updated it           state is the only input

This sounds like a small difference. It isn’t. Once it clicked for me, I stopped thinking about “updating the UI” as a step in my code. The UI updates itself. My job is keeping state correct.

What’s genuinely better

Previews that actually work. I can see a component in a dozen configurations — light mode, dark mode, loading state, error state, different text lengths — without running the app. XML previews were technically available but slow and unreliable enough that most teams ignored them. Compose previews are fast enough that I actually use them daily.

Reusability without boilerplate. Building a custom view in XML required custom attributes, obtainStyledAttributes, and careful lifecycle management. In Compose, a reusable component is a function with parameters. The simplicity is not a tradeoff — it’s a genuine improvement.

State-driven UI eliminates a whole class of bugs. The invisible-state bug — where UI shows the wrong thing because an update was applied out of order or missed entirely — mostly disappears. If the state is correct, the UI is correct. That guarantee changes how you debug.

What people don’t warn you about

Recomposition is subtle. If you pass unstable types to composables, Compose will recompose more than you expect. I’ve shipped screens that felt laggy in ways XML never produced. The fix is understanding remember, derivedStateOf, stable annotations, and what actually triggers recomposition. This knowledge is well-documented but requires deliberate investment — it won’t emerge naturally from reading the basics.

LazyColumn is not RecyclerView. The mental model is similar but the behavior differs. Missing key parameters in LazyColumn items causes jarring scroll-position resets when the list updates. This is documented, but it consistently surprises developers coming from RecyclerView because there’s no compile-time warning and the bug only appears at runtime under specific update patterns.

Module setup takes real effort. Adding Compose to an existing module requires careful Gradle configuration, especially in a multi-module project with shared dependencies. Budget time for setup, not just for learning. First integration in a large codebase took us a full day before the first composable rendered correctly.

How I migrated without breaking things

I didn’t rewrite existing screens. I picked the next new feature going into development and built it entirely in Compose. It shipped. The next new feature was also Compose. Gradually, all new work moved to Compose.

Existing XML screens stayed XML until there was a product reason to redesign them. At that point, the redesign used Compose. Never a pure technical rewrite for its own sake.

              legacy XML screens            new work in Compose
  month 0     ##############################
  month 12    ####################          ##########
  month 24    ###############               ###############
                                            ^ new features and
              ^ untouched, still shipping      redesigns only

Two years into this approach, over half our screens are Compose. The XML screens that remain are stable and tested. There’s no urgency to migrate them on principle — they work, and migration without a product trigger is churn.

The verdict

Compose is meaningfully better for new Android UI development. The learning curve is real but front-loaded. After two to three months of regular use, you stop fighting it and move faster than you ever did with XML.

The things that make it harder — recomposition behavior, LazyColumn quirks, Gradle setup — are learnable. The things that make it better — state-driven rendering, composability, preview tooling — are structural. The fundamentals are sound in a way that XML never was.

If you’re waiting for Compose to “mature”: it’s mature. The cost of waiting is now higher than the cost of learning.