Skip to main content

What This Skill Provides

The XML to Compose Migration skill helps you systematically convert Android XML layouts to Jetpack Compose while preserving functionality and embracing modern Compose patterns. This includes layout mapping, state migration, and incremental adoption strategies.

When to Use This Skill

  • Migrating existing XML-based UI to Compose
  • Converting Views to Composables
  • Modernizing legacy Android UI code
  • Planning incremental Compose adoption
  • Understanding View-to-Compose equivalents

Migration Workflow

1. Analyze the XML Layout

  • Identify the root layout type (ConstraintLayout, LinearLayout, FrameLayout, etc.)
  • List all View widgets and their key attributes
  • Map data binding expressions (@{}) or view binding references
  • Identify custom views that need special handling
  • Note any include, merge, or ViewStub usage

2. Plan the Migration

  • Decide: Full rewrite or incremental migration (using ComposeView/AndroidView)
  • Identify state sources (ViewModel, LiveData, savedInstanceState)
  • List reusable components to extract as separate Composables
  • Plan navigation integration if using Navigation component

3. Convert Layouts

Apply layout mappings to convert each View to its Compose equivalent.

4. Migrate State

  • Convert LiveData observation to StateFlow collection or observeAsState()
  • Replace findViewById / ViewBinding with Compose state
  • Convert click listeners to lambda parameters

5. Test and Verify

  • Compare visual output between XML and Compose versions
  • Test accessibility (content descriptions, touch targets)
  • Verify state preservation across configuration changes

Layout Mapping Reference

Container Layouts

XML
<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <!-- Children -->
</LinearLayout>
Compose
Column(
    modifier = Modifier.fillMaxWidth(),
    verticalArrangement = Arrangement.Top,
    horizontalAlignment = Alignment.Start
) {
    // Children
}

Common Widgets

XML
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textSize="18sp" />
Compose
Text(
    text = "Hello World",
    style = MaterialTheme.typography.bodyLarge
)

Quick Reference Table

XML WidgetCompose EquivalentNotes
TextViewTextUse style parameter
EditTextTextField / OutlinedTextFieldRequires state hoisting
ButtonButtonUse onClick lambda
ImageViewImageUse painterResource() or Coil
CheckBoxCheckboxRequires state hoisting
SwitchSwitchRequires state hoisting
ProgressBarCircularProgressIndicatorFor circular variant
RecyclerViewLazyColumn / LazyRowMost common migration
CardViewCardMaterial 3 component
ToolbarTopAppBarUse inside Scaffold

Attribute Mapping

Layout Attributes

<View
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
<LinearLayout android:orientation="horizontal">
    <View
        android:layout_width="0dp"
        android:layout_weight="1" />
    <View
        android:layout_width="0dp"
        android:layout_weight="2" />
</LinearLayout>

Visual Attributes

XML AttributeCompose Modifier
android:paddingModifier.padding()
android:layout_marginModifier.padding() on parent
android:backgroundModifier.background()
android:visibility="gone"Conditional composition
android:visibility="invisible"Modifier.alpha(0f)
android:clickableModifier.clickable { }
android:elevationModifier.shadow()
android:alphaModifier.alpha()
android:rotationModifier.rotate()
Visibility Mappingandroid:visibility="gone" → Remove from composition entirely (don’t render)android:visibility="invisible"Modifier.alpha(0f) (keeps space but invisible)

Common Migration Patterns

LinearLayout with Weights

<LinearLayout
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Left" />
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:text="Right" />
</LinearLayout>

RecyclerView to LazyColumn

class MyFragment : Fragment() {
    private lateinit var binding: FragmentMyBinding
    private lateinit var adapter: MyAdapter
    
    override fun onCreateView(...): View {
        binding = FragmentMyBinding.inflate(inflater)
        adapter = MyAdapter()
        binding.recyclerView.adapter = adapter
        
        viewModel.items.observe(viewLifecycleOwner) { items ->
            adapter.submitList(items)
        }
        
        return binding.root
    }
}

EditText with Two-Way Binding

<EditText
    android:text="@={viewModel.username}"
    android:hint="@string/username_hint"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

ConstraintLayout Migration

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/title"
        android:text="Title"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
    <TextView
        android:id="@+id/subtitle"
        android:text="Subtitle"
        app:layout_constraintTop_toBottomOf="@id/title"
        app:layout_constraintStart_toStartOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
For simple vertical/horizontal layouts, prefer Column/Row over ConstraintLayout in Compose for better performance.

Include / Merge to Composable

<!-- layout_header.xml -->
<merge>
    <ImageView
        android:id="@+id/avatar"
        android:layout_width="48dp"
        android:layout_height="48dp" />
    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</merge>

<!-- Usage -->
<include layout="@layout/layout_header" />

Incremental Migration (Interop)

Embedding Compose in XML

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

Embedding XML Views in Compose

AndroidView Wrapper
@Composable
fun MapViewComposable(
    modifier: Modifier = Modifier,
    onMapReady: (GoogleMap) -> Unit
) {
    AndroidView(
        factory = { context ->
            MapView(context).apply {
                onCreate(null)
                getMapAsync { map ->
                    onMapReady(map)
                }
            }
        },
        update = { mapView ->
            // Update the view when state changes
        },
        modifier = modifier
    )
}
Use AndroidView for Views that don’t have Compose equivalents yet, such as MapView, WebView, or custom third-party views.

State Migration

LiveData to StateFlow

class MyViewModel : ViewModel() {
    private val _uiState = MutableLiveData<UiState>()
    val uiState: LiveData<UiState> = _uiState
}

// In Fragment
viewModel.uiState.observe(viewLifecycleOwner) { state ->
    binding.title.text = state.title
}

Click Listeners to Lambdas

binding.submitButton.setOnClickListener {
    viewModel.submit()
}

Migration Checklist

  • All layouts converted (no include or merge left)
  • State hoisted properly (no internal mutable state for user input)
  • Click handlers converted to lambdas
  • RecyclerView adapters removed (using LazyColumn/LazyRow)
  • ViewBinding/DataBinding removed
  • Navigation integrated (NavHost or interop)
  • Theming applied (MaterialTheme)
  • Accessibility preserved (content descriptions, touch targets)
  • Preview annotations added for development
  • Old XML files deleted

References

Build docs developers (and LLMs) love