AppMama

blog / compose · architecture

Jetpack Compose vs XML: Which to Use for a New App

· 9 min read · AppMama

For a new Android app in 2026, use Jetpack Compose. That is Google’s recommendation, it is where new Jetpack and Material work lands first, and it removes a whole class of bugs that come from keeping XML layouts and Kotlin state in sync. XML views still matter, though: almost every large app has some, and a few things still have no Compose-native version. This guide covers the Jetpack Compose vs XML decision honestly: where each wins, how the two interoperate, what performance really looks like, and how to migrate an existing app without stopping feature work.

The short answer: Compose vs XML for a new app

Start a new app in Compose unless one of these is true:

Even in those cases the answer is usually “Compose, with a few AndroidView islands”, not “all XML”. Views are not deprecated and will keep working for years, but new UI APIs, samples and Material 3 Expressive components arrive in Compose first, and some arrive only there.

Default to Compose for every new screen. Use XML only where a View-only dependency forces it, and wrap that View with AndroidView instead of building the whole screen around it.

What is the difference between Compose and XML views?

With XML, you describe a tree of View objects in a layout file, inflate it, then mutate those objects from Kotlin as state changes: textView.text = ..., button.isEnabled = ..., adapter.notifyItemChanged(...). The UI is a long-lived object graph and your code is responsible for keeping it consistent with your data.

With Compose, UI is a Kotlin function of state. You describe what the screen should look like for the current state, and when that state changes the runtime re-runs (recomposes) only the functions that read it.

CartRow.kt

@Composable
fun CartRow(item: CartItem, onRemove: () -> Unit) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(item.name, modifier = Modifier.weight(1f))
        Text("x${item.quantity}")
        IconButton(onClick = onRemove, enabled = item.quantity > 0) {
            Icon(Icons.Default.Delete, contentDescription = "Remove")
        }
    }
}

There is no “find the view, then update it” step, so there is no way for the enabled state of the button to drift away from item.quantity. That one property, UI that cannot get out of sync with state, is most of the practical benefit.

Jetpack Compose vs XML at a glance

Jetpack ComposeXML views
UI modelDeclarative, functions of stateImperative, mutable View objects
ListsLazyColumn / LazyRow, a few linesRecyclerView + adapter + ViewHolder + DiffUtil
Custom componentsA new composable functionSubclass View, measure, draw, attrs.xml
ThemingMaterialTheme in Kotlinthemes.xml, styles, theme attributes
Animationanimate*AsState, AnimatedVisibility, shared elementsAnimators, MotionLayout, transitions
Previews@Preview, interactive preview, multiple configsLayout Editor
UI testscreateComposeRule(), semantics treeEspresso
Cold-start costHigher without Baseline ProfilesLower, framework is in the OS image
Ecosystem maturityVery good; a few SDKs still View-onlyEverything supports it

Is Jetpack Compose slower than XML?

Steady-state, a well-written Compose screen performs on par with an equivalent View screen, and complex lists are often easier to make fast in Compose because there is no adapter boilerplate to get wrong. The real differences are narrower than the internet suggests:

APK size goes up a little when you add Compose, and goes back down as you delete XML layouts, adapters and View-based libraries. With R8 enabled, it is rarely a deciding factor; see how to reduce Android app size if it is for you.

How do I set up Compose in a new project?

Since Kotlin 2.0, the Compose compiler ships with Kotlin as a Gradle plugin, so its version always matches your Kotlin version. Library versions come from the Compose BOM.

app/build.gradle.kts

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.plugin.compose")
}

android {
    buildFeatures {
        compose = true
    }
}

dependencies {
    // Check developer.android.com/develop/ui/compose/bom for the latest BOM
    val composeBom = platform("androidx.compose:compose-bom:2026.09.00")
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")
    implementation("androidx.activity:activity-compose:1.13.0")

    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}

The versions above are current as of November 2026; check the BOM mapping page for the latest, and declare them in a version catalog rather than inline. The plugins block assumes AGP 9, which has Kotlin support built in; on older AGP versions also apply org.jetbrains.kotlin.android. Recent Compose releases also require a recent compileSdk and Android Gradle Plugin, which is the most common reason a BOM bump fails to build.

How do Compose and XML views work together?

Interop works in both directions and is production-grade. This is what makes gradual migration possible.

Compose inside an XML layout: ComposeView

ComposeView is an ordinary View, so it can go anywhere in an XML layout:

res/layout/fragment_profile.xml

<androidx.compose.ui.platform.ComposeView
    android:id="@+id/compose_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

In a Fragment, set a composition strategy tied to the Fragment’s view lifecycle before calling setContent:

ProfileFragment.kt

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.composeView.apply {
        setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
        setContent {
            AppTheme {
                ProfileHeader(viewModel)
            }
        }
    }
}

The default strategy, DisposeOnDetachedFromWindowOrReleasedFromPool, disposes the composition when the view detaches from the window (and handles RecyclerView pooling). In Fragments the view can detach and reattach, for example on the back stack, so the default can throw away state you expected to keep. If you have more than one ComposeView in a layout, give each a unique ID or saved state will not restore correctly.

XML views inside Compose: AndroidView

Use AndroidView for a View with no Compose equivalent, such as a WebView, an ad view or a custom chart from an old library. Create the View in factory and push state into it in update, which re-runs whenever the state it reads changes:

@Composable
fun LegacyChart(points: List<Float>, modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context -> SparklineView(context) },
        update = { view -> view.setPoints(points) }
    )
}

Inside a LazyColumn, pass an onReset callback too; it opts the View into reuse, the same way RecyclerView recycles ViewHolders. To embed a whole existing XML layout, AndroidViewBinding from androidx.compose.ui:ui-viewbinding inflates a view binding class. To embed a Fragment during migration, AndroidFragment from androidx.fragment:fragment-compose does it with the right lifecycle handling.

One theme, two systems

The XML theme (themes.xml) and the Compose MaterialTheme are separate. A ComposeView does not inherit your XML colors, and an AndroidView does not see MaterialTheme. In a mixed app, define your colors once as resources or Kotlin constants and feed both. Even a pure Compose app still needs a minimal XML theme for the window background and splash screen. The Material 3 theming guide covers the Compose side in depth.

How do I migrate an existing XML app to Compose?

Do not rewrite. Migrate incrementally, and let the boundary move as you touch code anyway.

  1. Write all new screens in Compose. Host them in a Fragment with a ComposeView if your navigation is still Fragment-based. This stops the XML codebase from growing.
  2. Build shared components first. Recreate your design system (buttons, text fields, cards, empty states) as composables with your theme. Every later screen gets faster to build.
  3. Convert leaf UI next. Replace small pieces inside existing XML screens, such as a header, a list row or a dialog, with ComposeViews. Low risk, quick wins, and the team learns on real code.
  4. Move state into ViewModels with StateFlow. Screens that already expose a single UI state object convert easily; screens that poke Views from ten callbacks do not. Fix the architecture before the UI (see modern Android app architecture).
  5. Convert whole screens, then navigation. Once most screens are composables, move to Compose navigation in a single Activity and delete the Fragments.

Expect the awkward phase to be lists and complex custom Views. A RecyclerView with many view types usually maps to a LazyColumn with a when over item types, which is simpler, but do it as a dedicated change with screenshot tests in place.

Migration order that works: new screens, then shared components, then leaf UI, then state, then whole screens, then navigation. Skipping the state step is what makes migrations stall.

When is XML still the better choice?

Things that are no longer good reasons: “Compose is not stable” (it has been stable since 2021), “there is no ConstraintLayout” (there is, but Row/Column/Box cover most layouts without it), and “Compose cannot do edge-to-edge” (it handles window insets well, arguably better than Views).

What is the learning curve like?

For developers coming from Views, the syntax takes a day and the mental model takes a few weeks. The concepts that trip people up are:

Once those click, most teams report building screens noticeably faster than with XML, mainly because previews, theming and lists need so much less ceremony.

Frequently asked questions

Is XML deprecated in Android?

No. The View system and XML layouts are fully supported and will keep working. Google recommends Compose for new UI, and new UI features and samples target Compose first, but existing View code does not need to be rewritten.

Can I use Jetpack Compose and XML in the same app?

Yes, and most large apps do. ComposeView puts composables inside a View hierarchy or Fragment, and AndroidView puts a View inside a composable. You can mix them on the same screen.

Does Jetpack Compose make my app bigger?

Adding Compose adds some library code, but with R8 shrinking the increase is modest, and it shrinks again as you remove XML layouts, adapters and View-based UI libraries. Fully migrated apps are often smaller than before.

Why does my Compose app feel slow in debug builds?

Debug builds skip R8 optimization and Compose code is not precompiled on first run. Measure release builds, and add a Baseline Profile so critical code paths are compiled ahead of time at install.

Should I learn XML views or Compose first?

Learn Compose first if you are new to Android. Learn enough about Views to read old code and use AndroidView, since you will meet XML layouts in existing projects and some SDKs.