Android XML to Jetpack Compose: A Practical Conversion Guide with Real Examples

Android XML to Jetpack Compose: A Practical Conversion Guide with Real Examples

If you have been building Android apps for a while, there is a good chance that at least part of your UI still lives in XML.

And then comes the question:

How do I convert this XML layout to Jetpack Compose without rebuilding everything from scratch?

For a simple TextView, the answer is easy. For a real screen with nested LinearLayouts, weights, margins, styles, RecyclerView, custom views, and data binding, it gets more interesting.

The good news is that XML-to-Compose conversion becomes much easier once you stop looking for a one-to-one replacement for every XML tag and start thinking in terms of layout structure, state, and behavior.

In this guide, we’ll convert common Android XML patterns to Jetpack Compose with practical examples. We’ll also look at what can be converted automatically, what usually needs manual work, and how to handle an existing app where XML and Compose need to live together.


Quick XML to Jetpack Compose Reference

If you just need to know what a familiar XML component usually becomes in Compose, start here:

Android XML / ViewCommon Jetpack Compose Equivalent
LinearLayout verticalColumn
LinearLayout horizontalRow
FrameLayoutBox
TextViewText
EditTextTextField / OutlinedTextField
ButtonButton
ImageViewImage
RecyclerView verticalLazyColumn
RecyclerView horizontalLazyRow
ScrollViewColumn + verticalScroll()
android:paddingModifier.padding()
android:backgroundModifier.background()
android:clickableModifier.clickable()
layout_weightModifier.weight()
match_parentOften fillMaxWidth() / fillMaxSize()
visibility="gone"Conditional composition

Notice the word “common” in that table.

Compose is not XML written in Kotlin. Some concepts map neatly; others require a different approach.

Let’s convert them properly.


1. Convert a Vertical LinearLayout to Column

Here is a basic XML layout:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Welcome"
        android:textSize="24sp" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Continue" />

</LinearLayout>

A straightforward Compose version is:

@Composable
fun WelcomeSection() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Text(
            text = "Welcome",
            fontSize = 24.sp
        )

        Button(
            onClick = {},
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Continue")
        }
    }
}

The basic mapping is:

  • Vertical LinearLayout → Column

  • TextView → Text

  • Button → Button

  • android:padding="16dp" → Modifier.padding(16.dp)

  • match_parent width → Modifier.fillMaxWidth()

A horizontal LinearLayout normally becomes a Row.

Row {
    Text("Left")
    Text("Right")
}

That part is simple. Real layouts usually become harder when sizing and weights enter the picture.


2. What Happens to match_parent and wrap_content?

This is one of the first places where blindly translating XML causes confusion.

In XML you may have:

android:layout_width="match_parent"
android:layout_height="wrap_content"

In Compose, there isn’t a universal match_parent or wrap_content attribute that you copy across.

For width, you may use:

Modifier.fillMaxWidth()

For both available dimensions:

Modifier.fillMaxSize()

But many composables naturally size themselves according to their content and constraints, so you often don’t need to add anything to reproduce wrap_content behavior.

For example:

Text("Hello")

doesn’t need a wrapContentWidth() modifier simply because the old TextView used wrap_content.

This is an important rule for XML-to-Compose migration:

Convert the behavior of the layout, not just the attribute names.


3. Convert XML Padding and Margins to Compose

Suppose you have:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="12dp"
    android:layout_marginTop="16dp"
    android:text="Hello" />

A developer moving to Compose might expect separate padding and margin properties.

Compose handles this differently.

A common equivalent is:

Text(
    text = "Hello",
    modifier = Modifier
        .padding(top = 16.dp)
        .padding(12.dp)
)

But there is an important detail here: modifier order matters.

These two are not always equivalent:

Modifier
    .background(Color.Gray)
    .padding(16.dp)

and:

Modifier
    .padding(16.dp)
    .background(Color.Gray)

Compose modifiers form a chain. Changing their order can change the visual result and touch area.

This is one reason a mechanical XML conversion still needs developer review.


4. Convert layout_weight to Modifier.weight()

Consider two equally sized buttons in XML:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Cancel" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Save" />

</LinearLayout>

In Compose:

Row(
    modifier = Modifier.fillMaxWidth()
) {
    Button(
        onClick = {},
        modifier = Modifier.weight(1f)
    ) {
        Text("Cancel")
    }

    Button(
        onClick = {},
        modifier = Modifier.weight(1f)
    ) {
        Text("Save")
    }
}

The strange-looking XML combination:

android:layout_width="0dp"
android:layout_weight="1"

doesn’t need to be reproduced literally.

The important behavior is that both children divide the available horizontal space equally.

Compose expresses that directly with weight().


5. Convert FrameLayout to Box

FrameLayout is commonly used when elements need to overlap.

For example:

<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="200dp">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Featured" />

</FrameLayout>

In Compose, Box is normally a natural fit:

Box(
    modifier = Modifier
        .fillMaxWidth()
        .height(200.dp)
) {
    Image(
        painter = painterResource(R.drawable.header),
        contentDescription = null,
        modifier = Modifier.fillMaxSize()
    )

    Text(
        text = "Featured",
        modifier = Modifier.align(Alignment.TopStart)
    )
}

Again, we’re converting the purpose of the old container rather than translating its name.


6. Convert TextView to Text

A basic XML TextView:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello Android"
    android:textSize="18sp"
    android:textStyle="bold" />

can become:

Text(
    text = "Hello Android",
    fontSize = 18.sp,
    fontWeight = FontWeight.Bold
)

For production apps, avoid hard-coding text when it belongs in Android resources.

For example:

Text(
    text = stringResource(R.string.hello_android)
)

This keeps localization in place instead of losing it during conversion.


7. Convert EditText to TextField

This is where conversion starts involving state.

An XML field might look like this:

<EditText
    android:id="@+id/email"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Email address"
    android:inputType="textEmailAddress" />

A simple Compose version could be:

@Composable
fun EmailField() {
    var email by rememberSaveable {
        mutableStateOf("")
    }

    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = {
            Text("Email address")
        },
        modifier = Modifier.fillMaxWidth()
    )
}

The important difference isn’t EditText versus OutlinedTextField.

It is this:

value = email
onValueChange = { email = it }

Compose UI is driven by state.

In a real application, that state may belong in a ViewModel rather than directly inside the composable.

So a converter can generate the UI structure, but understanding where state should live requires knowledge of the application.


8. Convert RecyclerView to LazyColumn

A traditional Android list may use:

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/itemsList"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

But the XML is only a small part of the story.

A RecyclerView usually also requires an adapter, ViewHolder, item layout, and binding logic.

With Compose, a simple vertical list may look like:

@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(
            items = users,
            key = { user -> user.id }
        ) { user ->
            UserRow(user)
        }
    }
}

with:

@Composable
fun UserRow(user: User) {
    Text(
        text = user.name,
        modifier = Modifier.padding(16.dp)
    )
}

So converting RecyclerView is not really:

RecyclerView tag → LazyColumn tag

It can also mean moving UI responsibilities previously spread across an XML item layout, adapter and ViewHolder into composable functions.

That is a much more useful way to think about the migration.


9. What About ConstraintLayout?

Not every XML ConstraintLayout needs to become a Compose ConstraintLayout.

Take a screen that uses constraints only to put:

  1. a title at the top,

  2. an input underneath,

  3. a button underneath that.

A Column may express that UI more clearly.

On the other hand, a genuinely constraint-heavy design may still justify Compose ConstraintLayout.

The goal shouldn’t be:

“Which Compose class has the same name as my XML class?”

Instead ask:

“What is the simplest Compose structure that reproduces this UI correctly?”

This often produces cleaner Compose code.


10. Convert visibility=”gone” to Conditional UI

XML commonly uses:

android:visibility="gone"

In Compose, you often don’t create the UI at all when it shouldn’t be visible.

For example:

if (showError) {
    Text(
        text = "Something went wrong"
    )
}

When showError is false, that Text isn’t part of the composition.

For animated appearance and disappearance, you may use APIs such as AnimatedVisibility.

This illustrates another important point:

Some XML properties turn into Kotlin logic rather than Compose properties.


11. Converting a Small Real Screen

Let’s put several pieces together.

Imagine an old login screen:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="24dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Sign in"
        android:textSize="28sp"
        android:textStyle="bold" />

    <EditText
        android:id="@+id/email"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Email" />

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Password"
        android:inputType="textPassword" />

    <Button
        android:id="@+id/loginButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sign in" />

</LinearLayout>

A Compose version could be:

@Composable
fun LoginScreen(
    onLogin: (String, String) -> Unit
) {
    var email by rememberSaveable {
        mutableStateOf("")
    }

    var password by rememberSaveable {
        mutableStateOf("")
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Sign in",
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold
        )

        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            modifier = Modifier.fillMaxWidth()
        )

        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            modifier = Modifier.fillMaxWidth()
        )

        Button(
            onClick = {
                onLogin(email, password)
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Sign in")
        }
    }
}

Notice that this isn’t a literal line-by-line translation.

The screen’s intent was preserved, while the implementation was adapted to Compose.

That is usually what you want from a migration.


12. Can XML to Jetpack Compose Be Converted Automatically?

For many layouts, yes—as a starting point.

Straightforward structures such as:

  • LinearLayout

  • FrameLayout

  • TextView

  • Button

  • ImageView

  • common dimensions

  • padding

  • simple colors

  • common layout relationships

can often be translated into useful Compose code automatically.

If you have an existing XML layout and want a starting point instead of rewriting every element manually, you can use the ZedrooAI XML to Jetpack Compose Converter.

Paste your Android XML, generate the Compose version, then review the output in your project.

Automated conversion is especially useful for removing repetitive rewriting work.

But generated code should be treated as a migration starting point, not as code that should always be copied into production without review.


13. What an Automatic Converter Cannot Know About Your App

This is important.

A converter can see your XML.

It usually cannot fully understand everything around that XML.

For example:

  • Where should screen state live?

  • Which values should come from a ViewModel?

  • What business logic runs when a button is clicked?

  • Does a custom View have behavior not represented in XML?

  • Which accessibility behavior does the screen require?

  • Should a complex ConstraintLayout actually become several smaller composables?

  • Is an XML style part of a larger design system?

  • Are binding expressions connected to application-specific logic?

  • Does a generated layout behave correctly on every supported screen size?

That is why a good conversion workflow is:

Convert → Compile → Preview → Compare → Test → Refactor

not:

Convert → Paste → Ship


14. You Don’t Have to Convert the Entire App at Once

This is particularly useful for existing production apps.

Jetpack Compose and the traditional Android View system can coexist.

That means you can migrate one component or screen at a time instead of stopping development to rewrite the entire UI.

For example, Compose UI can be hosted inside an existing View-based screen using ComposeView.

Likewise, an existing Android View can be hosted inside Compose using AndroidView.

This is useful when you have something such as a custom View, WebView, ad view, or another component that isn’t practical to rewrite immediately.

So if you have a large XML application, a realistic plan may be:

  1. Choose a relatively isolated screen.

  2. Capture how the existing screen looks and behaves.

  3. Convert its layout to Compose.

  4. Connect the existing state and business logic.

  5. Compare the new UI with the original.

  6. Test it.

  7. Replace the old screen.

  8. Move on to the next suitable part of the app.

You don’t need a “rewrite everything this weekend” strategy.


15. Using Compose Inside an Existing XML Screen

During an incremental migration, an XML layout can contain a ComposeView.

A simplified example:

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

Then your Activity or Fragment can provide Compose content:

composeView.setContent {
    MaterialTheme {
        NewComposeSection()
    }
}

This allows a screen to remain partly View-based while a new section is written in Compose.

It is particularly useful when migrating an existing application gradually.


16. Using an Existing Android View Inside Compose

Sometimes the opposite problem appears.

Most of your new screen is Compose, but one existing View still needs to remain.

AndroidView can bridge that gap:

AndroidView(
    factory = { context ->
        MyCustomView(context)
    },
    update = { view ->
        // Update the View when Compose state changes
    }
)

There is also AndroidViewBinding for cases where existing View Binding/XML content needs to be used during migration.

This means conversion doesn’t have to be all-or-nothing.


17. Common XML-to-Compose Conversion Mistakes

Translating every XML attribute literally

Compose has a different UI model. Preserve behavior rather than forcing a one-to-one translation.

Ignoring modifier order

The order of padding(), background(), clickable(), sizing, clipping, and other modifiers can affect the result.

Putting all state inside composables

A small demo may use remember, while a real screen may need state hoisting or a ViewModel.

Recreating a complex XML hierarchy exactly

Nested XML containers sometimes exist because of limitations or patterns from the View system. Compose may allow a much simpler structure.

Assuming generated code is production-ready

Automatic conversion can save substantial typing, but compilation, visual verification, behavior testing, accessibility checks, and refactoring still matter.

Migrating a whole application in one pass

For an established application, gradual migration is often easier to validate and safer to ship.


18. A Practical XML-to-Compose Workflow

If I were converting an existing screen today, I would use this workflow:

Step 1 — Keep the original screen as the reference

Take a screenshot and note important interactions before changing anything.

Step 2 — Inspect the XML hierarchy

Identify containers, reusable components, styles, resources, custom views, and binding expressions.

Step 3 — Generate or write the initial Compose structure

For repetitive layout conversion, use an XML-to-Compose converter to create the first draft.

Step 4 — Fix semantics rather than syntax

Don’t ask only whether LinearLayout became Column. Check whether the new layout behaves like the original.

Step 5 — Connect real state

Move UI state to the appropriate place and reconnect existing ViewModel or application logic.

Step 6 — Preview and compare

Compare the Compose result with the original UI.

Step 7 — Test behavior

Check clicks, scrolling, input, configuration changes, accessibility, different screen sizes, and any important state transitions.

Step 8 — Refactor

Once parity is achieved, simplify code that still looks like XML translated into Kotlin.

This final step matters. Good Compose should eventually look like Compose—not like an XML tree wearing Kotlin syntax.


19. Should You Convert XML to Compose?

Not every working XML screen needs to be rewritten simply because Compose exists.

Migration makes more sense when:

  • you are actively modernizing the app,

  • a screen already needs significant UI work,

  • new features are being built in Compose,

  • maintaining two UI approaches is becoming expensive,

  • or the migration provides a clear development benefit.

For a stable legacy screen that rarely changes, immediate conversion may not be the highest-priority engineering task.

The better question is not:

“Can this XML be converted?”

It usually can.

The better question is:

“Is converting this screen useful right now, and what is the safest way to do it?”


Final Thoughts

Moving from Android XML to Jetpack Compose is not simply a matter of replacing tags:

LinearLayout becomes Column.

TextView becomes Text.

RecyclerView becomes LazyColumn.

Those mappings are useful, but the real migration involves layout behavior, state, theming, resources, interaction, and testing.

For a small layout, you can often convert the code manually in a few minutes.

For a larger existing XML screen, an automated converter can give you a useful first draft and remove much of the repetitive work. You can then spend your time on the part that actually requires developer judgment: making the resulting Compose UI correct, maintainable, and appropriate for your application.

If you already have an XML layout ready, try it with the ZedrooAI XML to Jetpack Compose Converter, then use the patterns in this guide to review and refine the generated Compose code.

The goal isn’t just to turn XML into Kotlin.

The goal is to end up with good Compose.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top