plugins/android-skills/skills/xml-to-compose-migration/SKILL.md
Use when migrating Android XML layouts to Jetpack Compose — converting Views to Composables, mapping layout attributes, migrating state from LiveData/ViewBinding, and planning incremental adoption via ComposeView.
npx skillsauth add rcosteira79/android-skills xml-to-compose-migrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Systematically convert Android XML layouts to idiomatic Jetpack Compose, preserving functionality while embracing Compose patterns.
ConstraintLayout, LinearLayout, FrameLayout, etc.)@{}) or ViewBinding referencesinclude, merge, or ViewStub usageComposeView)ViewModel, LiveData, savedInstanceState)Apply the mapping tables below to convert each View to its Compose equivalent.
LiveData → StateFlow + collectAsStateWithLifecycle()findViewById / ViewBinding references with Compose staterememberSaveable where needed)| XML | Compose | Notes |
|-----|---------|-------|
| LinearLayout (vertical) | Column | Use Arrangement and Alignment |
| LinearLayout (horizontal) | Row | Use Arrangement and Alignment |
| FrameLayout | Box | Children stack; last child is on top |
| ConstraintLayout | ConstraintLayout (Compose) | Use createRefs() and constrainAs |
| RelativeLayout | Box or ConstraintLayout | Prefer Box for simple overlap |
| ScrollView | Column + Modifier.verticalScroll() | Use LazyColumn for lists |
| HorizontalScrollView | Row + Modifier.horizontalScroll() | Use LazyRow for lists |
| RecyclerView | LazyColumn / LazyRow / LazyVerticalGrid | Most common migration |
| ViewPager2 | HorizontalPager | From androidx.compose.foundation |
| CoordinatorLayout + AppBarLayout | Scaffold + TopAppBar with scrollBehavior | |
| NestedScrollView | Column + Modifier.verticalScroll() | Prefer LazyColumn for large content |
| XML | Compose | Notes |
|-----|---------|-------|
| TextView | Text | Map style → TextStyle |
| EditText | TextField / OutlinedTextField | Requires state hoisting |
| Button | Button | Use onClick lambda |
| ImageView | Image (local) / AsyncImage (remote) | Use Coil for URLs |
| ImageButton | IconButton | Wrap Icon inside |
| CheckBox | Checkbox | Requires checked + onCheckedChange |
| RadioButton | RadioButton in a Column | Manage selected state externally |
| Switch | Switch | Requires state hoisting |
| ProgressBar (circular) | CircularProgressIndicator | |
| ProgressBar (horizontal) | LinearProgressIndicator | |
| SeekBar | Slider | Requires state hoisting |
| Spinner | ExposedDropdownMenuBox + DropdownMenuItem | More explicit pattern |
| CardView | Card | Material 3 |
| Toolbar | TopAppBar | Inside Scaffold |
| BottomNavigationView | NavigationBar | Material 3 |
| NavigationView (drawer) | ModalNavigationDrawer | Material 3 |
| FloatingActionButton | FloatingActionButton | Inside Scaffold |
| Divider | HorizontalDivider / VerticalDivider | |
| Space | Spacer + Modifier.size() | |
| XML Attribute | Compose Equivalent |
|---------------|--------------------|
| layout_width="match_parent" | Modifier.fillMaxWidth() |
| layout_height="match_parent" | Modifier.fillMaxHeight() |
| layout_width="wrap_content" | Default (implicit) |
| layout_weight | Modifier.weight(1f) |
| android:padding | Modifier.padding() |
| android:layout_margin | Modifier.padding() on parent, or Arrangement.spacedBy() |
| android:background | Modifier.background() |
| visibility="gone" | Conditional composition — don't emit the composable |
| visibility="invisible" | Modifier.alpha(0f) — keeps the space |
| android:clickable | Modifier.clickable { } |
| android:contentDescription | contentDescription parameter or Modifier.semantics |
| android:elevation | Modifier.shadow() or component elevation param |
| android:alpha | Modifier.alpha() |
| android:rotation | Modifier.rotate() |
| android:scaleX/Y | Modifier.scale() |
| android:gravity | Alignment / Arrangement parameters |
| android:textSize | fontSize in TextStyle |
| android:textColor | color in Text() |
| android:textStyle="bold" | fontWeight = FontWeight.Bold |
| android:maxLines | maxLines parameter in Text |
| android:ellipsize | overflow = TextOverflow.Ellipsis |
// Before: XML RecyclerView + Adapter
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// After: Compose
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items, key = { it.id }) { item ->
ItemRow(item = item, onItemClick = onItemClick)
}
}
// Before: LiveData in ViewModel
val userName: LiveData<String> = MutableLiveData("Alice")
// After: StateFlow in ViewModel
private val _userName = MutableStateFlow("Alice")
val userName: StateFlow<String> = _userName.asStateFlow()
// In composable:
val name by viewModel.userName.collectAsStateWithLifecycle()
// Before: ViewBinding
binding.loginButton.setOnClickListener {
viewModel.onLoginClicked(binding.emailInput.text.toString())
}
// After: Compose — state hoisted to screen, events flow up
LoginScreen(
uiState = uiState,
onLoginClick = viewModel::onLoginClicked
)
@Composable
fun LoginScreen(
uiState: LoginUiState,
onLoginClick: (String) -> Unit
) {
var email by remember { mutableStateOf("") }
TextField(value = email, onValueChange = { email = it })
Button(onClick = { onLoginClick(email) }) { Text("Log in") }
}
For large apps, migrate one screen at a time using ComposeView inside the existing XML Activity/Fragment:
<!-- In existing XML layout -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// In Activity or Fragment
val composeView = findViewById<ComposeView>(R.id.compose_view)
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
composeView.setContent {
AppTheme {
MyMigratedScreen(viewModel = viewModel())
}
}
Always set ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed in Fragments to avoid state leaks.
| Pitfall | Fix |
|---------|-----|
| visibility="gone" equivalent | Don't emit the composable at all — if (isVisible) { MyComposable() } |
| drawableLeft / drawableRight on TextView | Compose Row with Icon + Text |
| include / merge tags | Extract as a composable function |
| ViewStub (lazy inflation) | Conditional composable wrapped in if |
| savedInstanceState in Fragment | rememberSaveable in composable |
| Shared element transitions between Activities | Compose shared element transitions (sharedElement modifier, Navigation 2.8+) |
| Custom View subclass | Wrap in AndroidView initially; rewrite to composable over time |
testing
Use when implementing paginated lists in Android or Compose with Paging 3 — PagingSource, Pager and PagingConfig setup, RemoteMediator for offline-first lists, LazyPagingItems and itemKey integration in LazyColumn, dynamic filters via flatMapLatest, and unit tests with TestPager and asSnapshot. Triggers include Paging 3, infinite list, infinite scroll, paginated list, LazyPagingItems, collectAsLazyPagingItems, and cachedIn.
development
Use when setting up or working with Koin in Android or KMP projects — module declarations with Classic DSL or KSP annotations, ViewModel injection in Compose, scopes, Nav 3 entry providers, application startup, and compile-time verification via `verify()`. Triggers on Koin, `single`, `factory`, `koinViewModel`, `koinInject`, `parametersOf`, `startKoin`, "KMP DI", "shared DI".
development
Use when persisting key-value preferences or small typed settings on Android or KMP with Jetpack DataStore — Preferences vs Typed (Proto/JSON) selection, KMP factory with per-platform file paths, SharedPreferences migration, serializers with corruption handlers, DI singletons, and repository/MVI integration. Triggers on DataStore, Preferences, PreferenceDataStoreFactory, DataStoreFactory, preferencesDataStore, SharedPreferencesMigration, Serializer, or persistent settings work.
development
Use when writing, fixing, or refactoring Android/KMP code in Kotlin — supplements superpowers:test-driven-development with Android's three-tier test model, fake-first strategy, coroutine testing, and Compose UI testing.