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
LinearLayout
FrameLayout
RecyclerView
< LinearLayout
android:orientation = "vertical"
android:layout_width = "match_parent"
android:layout_height = "wrap_content" >
<!-- Children -->
</ LinearLayout >
Column (
modifier = Modifier. fillMaxWidth (),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
// Children
}
< FrameLayout
android:layout_width = "match_parent"
android:layout_height = "match_parent" >
<!-- Stacked children -->
</ FrameLayout >
Box (
modifier = Modifier. fillMaxSize ()
) {
// Stacked children
}
< androidx.recyclerview.widget.RecyclerView
android:id = "@+id/recyclerView"
android:layout_width = "match_parent"
android:layout_height = "match_parent" />
LazyColumn (
modifier = Modifier. fillMaxSize ()
) {
items (items, key = { it.id }) { item ->
ItemRow (item = item)
}
}
TextView
EditText
Button
ImageView
< TextView
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:text = "Hello World"
android:textSize = "18sp" />
Text (
text = "Hello World" ,
style = MaterialTheme.typography.bodyLarge
)
< EditText
android:id = "@+id/usernameInput"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:hint = "Username" />
var username by remember { mutableStateOf ( "" ) }
OutlinedTextField (
value = username,
onValueChange = { username = it },
label = { Text ( "Username" ) },
modifier = Modifier. fillMaxWidth ()
)
< Button
android:id = "@+id/submitButton"
android:layout_width = "wrap_content"
android:layout_height = "wrap_content"
android:text = "Submit" />
Button (onClick = { /* Handle click */ }) {
Text ( "Submit" )
}
< ImageView
android:id = "@+id/profileImage"
android:layout_width = "48dp"
android:layout_height = "48dp"
android:src = "@drawable/profile" />
Image (
painter = painterResource (R.drawable.profile),
contentDescription = "Profile picture" ,
modifier = Modifier. size ( 48 .dp)
)
Quick Reference Table
XML Widget Compose Equivalent Notes 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
match_parent
Compose Equivalent
< View
android:layout_width = "match_parent"
android:layout_height = "match_parent" />
layout_weight
Compose Equivalent
< 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 Attribute Compose Modifier android:paddingModifier.padding()android:layout_marginModifier.padding() on parentandroid: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 Mapping android: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
XML Version
Compose Version
< 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
Before: RecyclerView
After: 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
XML with Data Binding
Compose Version
< EditText
android:text = "@={viewModel.username}"
android:hint = "@string/username_hint"
android:layout_width = "match_parent"
android:layout_height = "wrap_content" />
ConstraintLayout Migration
XML ConstraintLayout
Compose ConstraintLayout
< 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
XML with Include
Compose Reusable Component
<!-- 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
Add ComposeView to Layout
Initialize ComposeView
< 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
@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
Before: LiveData
After: 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
Before: setOnClickListener
After: onClick Lambda
binding.submitButton. setOnClickListener {
viewModel. submit ()
}
Migration Checklist
References