Jetpack Compose vs XML Views: Which Should You Use?

Quick answer: Choose Jetpack Compose for new Android UI and new screens in most apps. Keep XML Views where they already work well, then migrate incrementally when there is a clear product or maintenance benefit. You do not need to rewrite a healthy View-based app to start using Compose.
Jetpack Compose and XML Views are both supported Android UI approaches. The important choice is not which one is universally “better”; it is which one reduces risk and makes the next feature easier for your team to build and maintain.
Android supports interoperability in both directions: a ComposeView can host Compose inside a View hierarchy, and AndroidView or AndroidViewBinding can host an existing View inside Compose. That makes a gradual transition practical rather than all-or-nothing. See the official interoperability overview for the supported APIs.
The decision in 60 seconds
| Your situation | Start with | Why |
|---|---|---|
| A new app or a new full-screen feature | Jetpack Compose | You can build the screen with a declarative, Kotlin-first API from day one. |
| A stable app built with XML Views | Compose for new or isolated work | You gain Compose experience without a risky rewrite. |
| A View-based screen that needs one new modern component | ComposeView inside the existing screen | It creates a small migration boundary. |
| A Compose screen that needs a View-only widget or mature SDK view | AndroidView or AndroidViewBinding | You can keep the existing View while the rest of the screen stays in Compose. |
| A large, business-critical legacy screen | Keep Views until a feature or maintenance need justifies change | Migration itself is work; it should solve a real problem. |
For an existing app, Android’s migration guidance is to let Views and Compose coexist while you migrate incrementally. A common sequence is to build new screens in Compose, extract reusable Compose UI as features are built, and replace older screens one at a time. Android’s migration guide explains this approach.
What actually changes between Compose and XML Views?
The central difference is the UI programming model.
With Views, you normally describe a hierarchy in XML, inflate it, keep references to widgets, and change those widgets through methods such as setText() or setVisibility().
With Compose, a composable function describes the UI for the current state. When state changes, Compose can call the relevant functions again with the new input. This is declarative UI: state flows down to the UI, and user events flow back up to code that changes that state.
The Compose mental model is worth learning early: composable functions should be fast and free of side effects because they can run again, be skipped, or run in a different order.
| Area | XML Views | Jetpack Compose |
|---|---|---|
| UI definition | XML resources plus View objects | Kotlin composable functions |
| Typical UI update | Find a widget and mutate it | Update state and describe the new UI |
| Reuse | Custom views, includes, styles, adapters | Composable functions, parameters, and slots |
| Dynamic content | Often involves adapters, visibility changes, and View updates | Standard Kotlin if, when, and loops inside UI code |
| Existing app adoption | Already available in legacy apps | Can be introduced screen by screen |
| Interoperability | Can host ComposeView | Can host Views through AndroidView |
Neither column means that one toolkit cannot build a capable production UI. The practical benefit of Compose is the way its state-driven model keeps UI code close to the data and events that define it.
The same small feature, written two ways
Consider a profile action that changes from “Follow” to “Following.” In a View-based screen, the XML defines the widgets and Kotlin changes a particular Button when the user taps it.
<!-- res/layout/profile_action.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ada Lovelace" />
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Follow" />
</LinearLayout>import android.os.Bundle
import android.view.View
import android.widget.Button
private var isFollowing = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val followButton = view.findViewById<Button>(R.id.followButton)
followButton.setOnClickListener {
isFollowing = !isFollowing
followButton.text = if (isFollowing) "Following" else "Follow"
}
}The Compose version makes the state-to-UI relationship explicit. The screen does not look up a button and mutate it; it passes the current state to Button.
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun FollowProfile() {
var isFollowing by remember { mutableStateOf(false) }
Button(onClick = { isFollowing = !isFollowing }) {
Text(text = if (isFollowing) "Following" else "Follow")
}
}For a production screen, that state may come from a ViewModel instead of remember. The key idea stays the same: the UI displays the state it receives and exposes events that request a state change. State hoisting in Jetpack Compose shows how to move mutable state to the right owner.
When Compose is the better default
Start a new screen in Compose when the team is comfortable with Kotlin and you are free to choose the UI layer. It is particularly useful when the screen has:
- Several states, such as loading, content, empty, error, or signed-out.
- Repeated components that can become parameterized composables.
- Dynamic lists or conditional sections.
- A design system you want to express as reusable UI components.
- New work that you do not want to split between an XML layout and its UI logic.
Compose does not remove the need for architecture, state management, accessibility, or tests. It changes the way your UI consumes those concerns. Before moving beyond basic components, learn how composable functions work and how the Compose BOM setup keeps Compose library versions aligned.
When keeping XML Views is the sensible choice
Do not treat existing XML as technical debt simply because Compose exists. Keeping a View-based screen can be reasonable when:
- The screen is stable, well tested, and has no active feature work.
- A migration would be large but offer little user-facing or maintenance value.
- Your team needs time to establish Compose conventions and test coverage.
- A critical dependency is already represented as a View and wrapping it is simpler than rewriting around it.
This is a prioritization decision, not a permanent ban on Compose. A healthy migration plan gives every rewrite a purpose: shipping a new feature, simplifying a difficult screen, creating a reusable component, or retiring a maintenance problem.
Use interoperability as a boundary, not a tangle
The most useful mixed-UI pattern is usually a clear boundary: one screen, one feature section, or one reusable component at a time.
Put Compose inside an existing View screen
Use ComposeView when an XML layout or Fragment is still the host but a section of its content is ready for Compose. In a Fragment, set a composition strategy that follows the Fragment view lifecycle.
import android.os.Bundle
import android.view.View
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
class AccountFragment : Fragment(R.layout.fragment_account) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ComposeView>(R.id.account_compose_view).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
AccountSummary(name = "Ada")
}
}
}
}ComposeView is the supported entry point for this direction. Review the Compose-in-Views documentation before adding it to fragments, lists, or pooled containers, because composition disposal and state restoration need deliberate handling.
Put a View inside a Compose screen
Use AndroidView when a Compose screen needs an existing Android View.
import android.widget.ProgressBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun LegacyProgressIndicator(progress: Int) {
AndroidView(
factory = { context -> ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal) },
update = { progressBar -> progressBar.progress = progress }
)
}Keep the factory focused on creating the View and use update for state that may change. For layouts that already use View Binding, AndroidViewBinding is often a clearer fit. The Views-in-Compose guide covers both patterns.
Migration mistakes to avoid
Rewriting the entire app before learning the model
A “big bang” rewrite creates delivery and testing risk. Start with a new screen or a contained component, then use what you learn to define conventions for themes, state, navigation, and tests.
Treating composables like mutable widgets
Avoid looking for an equivalent to every setter call. Instead, identify the state, pass it into a composable, and send events through callbacks. This leads naturally to the one-way data flow recommended in Compose UI architecture.
Mixing toolkits deeply in the same feature
Interoperability is a migration tool, not a goal. A few deliberate boundaries are easier to test, theme, and eventually remove than nested View/Compose/View layers.
Promising a performance win without measuring
Compose and Views both require thoughtful UI design. Do not migrate solely because you expect automatic speed improvements. Measure a real problem before and after a targeted change; our Compose performance guide is a useful starting point.
Recommended path from here
- If you are new to Compose, read What Is Jetpack Compose? and build one small screen from scratch.
- If you maintain an XML app, choose one low-risk new feature or isolated section and introduce Compose there.
- Learn the state-driven model before converting complex forms or lists.
- Establish your team’s theme, testing, and navigation patterns before broad migration.
For a step-by-step migration walkthrough, including more detail on mixed screens, see Android View to Compose Migration: Complete Developer Guide.
FAQ
Is Jetpack Compose replacing XML completely?
Compose is Android’s modern declarative UI toolkit, but the View system remains supported and interoperable. Existing apps can adopt Compose incrementally instead of replacing every XML layout at once.
Should I migrate all XML layouts before adding new features?
No. New Compose screens can coexist with existing Views. In practice, shipping new work in Compose and migrating older screens when there is a clear reason usually creates less risk than pausing feature work for a full rewrite.
Is Compose always faster than XML Views?
No universal performance conclusion follows from the toolkit alone. UI performance depends on the screen, state usage, layout work, drawing, and the devices you support. Profile the specific user flow you want to improve.
Can I use a traditional View in Jetpack Compose?
Yes. AndroidView and AndroidViewBinding let a Compose hierarchy host existing View-based UI. In the opposite direction, ComposeView hosts Compose content in a View hierarchy.