Migrating from XML Views to Compose — Gradual Adoption Strategy
Migrate from XML Views to Jetpack Compose incrementally — ComposeView in Fragments, AndroidView for legacy widgets, navigation interop, and a practical migration roadmap.
This is Part 10 (final) of a 10-part series on Jetpack Compose.
- Your First Screen
- State Management
- Navigation
- Lists
- Theming
- Forms
- Side Effects
- Custom Layouts
- API Integration
- Migration from XML (this post)
You have an existing Android app with XML layouts, Fragments, and the View system. You want to adopt Compose. But you can’t rewrite everything at once.
The good news: Compose and Views are fully interoperable. You can adopt Compose screen by screen, or even widget by widget. This post shows you how.
The migration strategy
Don’t rewrite the entire app. Migrate incrementally:
Phase 1: Add Compose to new screens only
Phase 2: Replace simple existing screens
Phase 3: Migrate complex screens
Phase 4: Remove Fragment/XML infrastructure
Most apps stay in Phase 1–3 permanently. You don’t have to reach Phase 4.
Phase 1: Compose inside existing Fragments
ComposeView in XML
Add Compose content inside an existing Fragment layout:
<!-- fragment_profile.xml -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- Existing XML toolbar -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="Profile" />
<!-- Compose content replaces the rest -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
class ProfileFragment : Fragment(R.layout.fragment_profile) {
private val viewModel: ProfileViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<ComposeView>(R.id.compose_view).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
ProfileContent(viewModel = viewModel)
}
}
}
}
}
@Composable
fun ProfileContent(viewModel: ProfileViewModel) {
val state by viewModel.state.collectAsStateWithLifecycle()
// Compose UI here
}
Important: Set ViewCompositionStrategy to control when Compose disposes. DisposeOnViewTreeLifecycleDestroyed is correct for Fragments.
Full Compose Fragment
For new screens, use a Fragment that’s entirely Compose:
class SettingsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
AppTheme {
SettingsScreen()
}
}
}
}
}
No XML layout at all. The Fragment is just a Compose host.
Phase 2: Views inside Compose (AndroidView)
When you need to use a View-based widget in a Compose screen (MapView, AdView, custom Views):
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun MapScreen(location: LatLng) {
AndroidView(
factory = { context ->
MapView(context).apply {
onCreate(null)
getMapAsync { googleMap ->
googleMap.moveCamera(CameraUpdateFactory.newLatLng(location))
}
}
},
update = { mapView ->
mapView.getMapAsync { googleMap ->
googleMap.moveCamera(CameraUpdateFactory.newLatLng(location))
}
},
modifier = Modifier.fillMaxSize()
)
}
factorycreates the View onceupdateruns when Compose state changes- The View lifecycle is managed by Compose
WebView in Compose
@Composable
fun WebContent(url: String) {
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient()
loadUrl(url)
}
},
update = { webView ->
webView.loadUrl(url)
},
modifier = Modifier.fillMaxSize()
)
}
Existing custom Views
@Composable
fun LegacyChart(data: List<DataPoint>) {
AndroidView(
factory = { context -> CustomChartView(context) },
update = { chartView -> chartView.setData(data) },
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
}
This lets you reuse existing custom Views without rewriting them in Compose.
Navigation interop
Fragment navigation → Compose screen
If you use Navigation Component with Fragments, add Compose destinations:
// In your nav_graph.xml, keep Fragment destinations
// Add Compose destinations programmatically
val navController = findNavController()
// Navigate from Fragment to Compose screen
navController.navigate(R.id.settingsCompose)
Mixed navigation graph
NavHost(navController = navController, startDestination = "home") {
// Compose screen
composable("home") { HomeScreen() }
// Fragment screen (via AndroidViewBinding or Fragment)
composable("legacy_profile") {
AndroidViewBinding(FragmentProfileBinding::inflate)
}
// Another Compose screen
composable("settings") { SettingsScreen() }
}
Gradual navigation migration
- Keep your existing Fragment-based
NavHostFragment - New screens are Compose-only Fragments (just a
ComposeView) - Eventually, switch the root to Compose Navigation
- Migrate remaining Fragments to composables
Theme bridging
Using Material3 Compose theme with XML Views
// Wrap your Compose theme to provide values to XML child views
@Composable
fun AppThemeWithBridge(content: @Composable () -> Unit) {
AppTheme {
// MDC-Android Compose Theme Adapter bridges Compose ↔ XML themes
content()
}
}
For the transition period, use the Material Theme Adapter to keep Compose and XML themes in sync.
Simpler approach: Define colors once
// colors.kt — single source of truth
val PrimaryColor = Color(0xFF6200EA)
// Use in Compose
MaterialTheme(
colorScheme = lightColorScheme(primary = PrimaryColor)
)
// Use in XML via resources
// res/values/colors.xml
// <color name="primary">#6200EA</color>
Keep both in sync manually during migration. Once migration is complete, remove the XML resources.
State sharing between View and Compose
ViewModel shared between Fragment and Compose
class SharedViewModel : ViewModel() {
val items = MutableStateFlow<List<Item>>(emptyList())
}
// In Fragment (XML part)
class ListFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch {
viewModel.items.collect { items ->
adapter.submitList(items)
}
}
}
}
// In Compose part
@Composable
fun DetailScreen(viewModel: SharedViewModel = viewModel()) {
val items by viewModel.items.collectAsStateWithLifecycle()
// Use items
}
Both the Fragment and the Compose screen observe the same ViewModel. This works because ViewModel is lifecycle-independent.
Migration checklist per screen
For each screen you migrate:
- Identify the screen’s state — What data does it display? What user actions does it handle?
- Create a ViewModel (if not existing) with StateFlow
- Write the Compose UI — Start with the main content, ignore toolbar/navigation
- Replace the Fragment body — Use ComposeView
- Test — Verify state, navigation, and lifecycle behavior
- Remove XML — Delete the layout file and view binding references
What to migrate first
| Migrate first | Migrate last |
|---|---|
| New screens (no existing code) | Screens with complex custom Views |
| Simple list screens | Screens with MapView, WebView, Camera |
| Settings / preferences | Screens with heavy animation |
| Profile / about pages | Screens using third-party View libraries |
Start with screens that have simple layouts and no custom Views. Save complex screens with Maps, WebViews, or camera for later.
When NOT to migrate
- Third-party View SDKs (ads, maps, video players) — wrap with
AndroidViewand keep them - Complex custom Views with years of refinement — wrap, don’t rewrite
- The app is in maintenance mode — if no active development, migration adds risk with no benefit
Summary
Compose migration is incremental:
- ComposeView — embed Compose in existing Fragments
- AndroidView — embed Views in Compose screens
- Shared ViewModel — bridge state between systems
- Navigation interop — mixed Compose + Fragment navigation
Start with new screens. Migrate existing screens as you modify them. Keep third-party Views wrapped in AndroidView. There’s no deadline — Compose and Views coexist comfortably.
This concludes the 10-part Jetpack Compose series. You now have the tools to build production Compose apps — from basic layouts to API integration to migrating an existing app.