Awesome-android-agent-skills xml-to-compose-migration
Convert Android XML layouts to Jetpack Compose. Use when asked to migrate Views to Compose, convert XML to Composables, or modernize UI from View system to Compose.
install
source · Clone the upstream repo
git clone https://github.com/new-silvermoon/awesome-android-agent-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/new-silvermoon/awesome-android-agent-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.github/skills/migration/xml-to-compose-migration" ~/.claude/skills/new-silvermoon-awesome-android-agent-skills-xml-to-compose-migration && rm -rf "$T"
manifest:
.github/skills/migration/xml-to-compose-migration/SKILL.mdsource content
XML to Compose Migration
Overview
Systematically convert Android XML layouts to idiomatic Jetpack Compose, preserving functionality while embracing Compose patterns. This skill covers layout mapping, state migration, and incremental adoption strategies.
Workflow
1. Analyze the XML Layout
- Identify the root layout type (
,ConstraintLayout
,LinearLayout
, etc.).FrameLayout - 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
, ormerge
usage.ViewStub
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 the layout mapping table below to convert each View to its Compose equivalent.
4. Migrate State
- Convert
observation toLiveData
collection orStateFlow
.observeAsState() - Replace
/ ViewBinding with Compose state.findViewById - 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 Layout | Compose Equivalent | Notes |
|---|---|---|
| | Use and |
| | Use and |
| | Children stack on top of each other |
| (Compose) | Use and |
| or | Prefer Box for simple overlap |
| + | Or use for lists |
| + | Or use for lists |
| / / | Most common migration |
| | From accompanist or Compose Foundation |
| Custom + | Use with scroll behavior |
| + | Prefer Lazy variants |
Common Widgets
| XML Widget | Compose Equivalent | Notes |
|---|---|---|
| | Use → |
| / | Requires state hoisting |
| | Use lambda |
| | Use or Coil |
| | Use inside |
| | Requires + |
| | Use with for groups |
| | Requires state hoisting |
| | |
| | |
| | Requires state hoisting |
| + | More complex pattern |
| | From Material 3 |
| | Use inside |
| | Material 3 |
| | Use inside |
| / | |
| | Use |
Attribute Mapping
| XML Attribute | Compose Modifier/Property |
|---|---|
| |
| |
| (usually implicit) |
| |
| |
| on parent, or use |
| |
| Conditional composition (don't emit) |
| (keeps space) |
| |
| |
| or component's param |
| |
| |
| |
| parameter or |
| |
Common Patterns
LinearLayout with Weights
<!-- XML --> <LinearLayout android:orientation="horizontal"> <View android:layout_weight="1" /> <View android:layout_weight="2" /> </LinearLayout>
// Compose Row(modifier = Modifier.fillMaxWidth()) { Box(modifier = Modifier.weight(1f)) Box(modifier = Modifier.weight(2f)) }
RecyclerView to LazyColumn
<!-- XML --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" />
// Compose LazyColumn(modifier = Modifier.fillMaxSize()) { items(items, key = { it.id }) { item -> ItemRow(item = item, onClick = { onItemClick(item) }) } }
EditText with Two-Way Binding
<!-- XML with Data Binding --> <EditText android:text="@={viewModel.username}" android:hint="@string/username_hint" />
// Compose val username by viewModel.username.collectAsState() OutlinedTextField( value = username, onValueChange = { viewModel.updateUsername(it) }, label = { Text(stringResource(R.string.username_hint)) }, modifier = Modifier.fillMaxWidth() )
ConstraintLayout Migration
<!-- XML --> <androidx.constraintlayout.widget.ConstraintLayout> <TextView android:id="@+id/title" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/subtitle" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="@id/title" /> </androidx.constraintlayout.widget.ConstraintLayout>
// Compose ConstraintLayout(modifier = Modifier.fillMaxWidth()) { val (title, subtitle) = createRefs() Text( text = "Title", modifier = Modifier.constrainAs(title) { top.linkTo(parent.top) start.linkTo(parent.start) } ) Text( text = "Subtitle", modifier = Modifier.constrainAs(subtitle) { top.linkTo(title.bottom) start.linkTo(title.start) } ) }
Include / Merge → Extract Composable
<!-- XML: layout_header.xml --> <merge> <ImageView android:id="@+id/avatar" /> <TextView android:id="@+id/name" /> </merge> <!-- Usage --> <include layout="@layout/layout_header" />
// Compose: Extract as a reusable Composable @Composable fun HeaderSection( avatarUrl: String, name: String, modifier: Modifier = Modifier ) { Row(modifier = modifier) { AsyncImage(model = avatarUrl, contentDescription = null) Text(text = name) } } // Usage HeaderSection(avatarUrl = user.avatar, name = user.name)
Incremental Migration (Interop)
Embedding Compose in XML
<!-- In your XML layout --> <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" android:layout_width="match_parent" android:layout_height="wrap_content" />
// In Fragment/Activity binding.composeView.setContent { MaterialTheme { MyComposable() } }
Embedding XML Views in Compose
// Use AndroidView for Views that don't have Compose equivalents @Composable fun MapViewComposable(modifier: Modifier = Modifier) { AndroidView( factory = { context -> MapView(context).apply { // Initialize the view } }, update = { mapView -> // Update the view when state changes }, modifier = modifier ) }
State Migration
LiveData to Compose
// Before: Observing in Fragment viewModel.uiState.observe(viewLifecycleOwner) { state -> binding.title.text = state.title } // After: Collecting in Compose @Composable fun MyScreen(viewModel: MyViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Text(text = uiState.title) }
Click Listeners
// Before: XML + setOnClickListener binding.submitButton.setOnClickListener { viewModel.submit() } // After: Lambda in Compose Button(onClick = { viewModel.submit() }) { Text("Submit") }
Checklist
- All layouts converted (no
orinclude
left)merge - 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