SuperAPI AI Directory

xml-to-compose-migration

General & other

View repository

Updated 10-16 09:48

About this skill

XML to Compose Migration

Systematically convert Android XML layouts to idiomatic Jetpack Compose, preserving functionality while embracing Compose patterns.

Workflow

1. Analyse 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 ViewBinding references
  • Identify custom views that need special handling
  • Note any include, merge, or ViewStub usage

2. Plan the Migration

  • Decide: full rewrite or incremental (embedding Compose into existing XML via ComposeView)
  • Identify state sources (ViewModel, LiveData, savedInstanceState)
  • List reusable components to extract as separate composables
  • Plan navigation integration if the screen uses the Navigation component

3. Convert Layouts

Apply the mapping tables below to convert each View to its Compose equivalent.

4. Migrate State

  • Convert LiveData → StateFlow + collectAsStateWithLifecycle()
  • Replace findViewById / ViewBinding references with Compose state
  • Convert click listeners to lambda parameters passed down from the screen composable

5. Test and Verify

  • Compare visual output between XML and Compose versions
  • Test accessibility (content descriptions, touch targets, TalkBack traversal)
  • Verify state survives configuration changes (rememberSaveable where needed)

Layout Mapping

Container Layouts

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

Common Widgets

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()

Attribute Mapping

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

Common Migration Patterns

RecyclerView → LazyColumn

// 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)
    }
}

LiveData → StateFlow

// 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()

ViewBinding click listener → lambda

// 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") }
}

Incremental migration with ComposeView

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.


Gotchas

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