Learn Jetpack Compose - Interop & Migration
Episode 16 of 23

Learn Jetpack Compose - Interop & Migration

This episode bridges Compose and legacy applications: ComposeView in XML layouts, AndroidView for custom views, gradual screen migration, and hybrid architecture patterns during the transition.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Not every application can be rewritten overnight. Long-running applications — with dozens of screens and a large user base — need a smooth adoption path. Compose interop answers this: Compose and traditional views can coexist.

The approach is gradual migration: start with one screen, then spread. During the transition, the application runs hybrid — partly Compose, partly XML — without breaking features.

Episode 16 covers ComposeView, AndroidView, gradual migration, and hybrid architecture patterns.

Using ComposeView in XML Applications

Embedding Compose into an XML Layout

The fastest way to start: put a ComposeView in an XML layout and fill it with a composable:

activity_pembayaran.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compass_pembayaran"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
 
</LinearLayout>

ComposeView is treated like a regular view in XML. In code, you fill its Compose content:

KotlinMengisi ComposeView
findViewById<ComposeView>(R.id.compass_pembayaran).setContent {
    PembayaranScreen()
}

ComposeView.setContent { PembayaranScreen() } opens a composition context inside the view. XML handles the screen skeleton, while the payment part is written in Compose.

Composition inside a Fragment

For Fragment-based applications, use ComposeView inside onViewCreated:

KotlinComposeView di fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    view.findViewById<ComposeView>(R.id.komposisi_profil)
        .setContent {
            ProfilScreen()
        }
}

This pattern lets you move one Fragment at a time to Compose without changing the application's overall navigation.

Using AndroidView for Legacy Views

Embedding Views into Compose

The opposite direction: legacy views that can't be moved yet can be embedded into a composable with AndroidView:

KotlinAndroidView untuk view kustom
@Composable
fun PetaKustom() {
    AndroidView(
        factory = { context ->
            GoogleMapView(context)
        },
        modifier = Modifier.fillMaxSize()
    )
}

AndroidView(factory = { ... }) creates a view inside composition. update can be added to update the view when state changes:

KotlinAndroidView dengan update
AndroidView(
    factory = { context -> WebView(context) },
    update = { webView ->
        webView.loadUrl(alamatSitus)
    }
)

update = { webView -> webView.loadUrl(alamatSitus) } applies state changes to the view. This pattern handles libraries that don't have a Compose version yet — like maps and players.

InteropState and Lifecycle

For views that depend on the lifecycle, use AndroidView with LocalLifecycleOwner or the androidx interop library. The details matter when a view has long-lived listeners — combine it with DisposableEffect from episode 10 to clean them up.

Migrating Screens Gradually

Start with the Screens That Benefit Most

Rank which screens benefit most from Compose: screens with dynamic state and complex UI give the fastest returns. Simple static screens can stay longer.

The process: create the new screen in Compose, then change the navigation to point to that Compose screen. The application structure outside that screen keeps running. Make small commits per migrated screen, for example git commit -m "feat: migrate layar pembayaran", so rollback is always easy if there's a regression.

Preserving the Back Stack

Legacy navigation is based on Fragments and Activities. During the transition, the two systems run side by side: legacy Fragments display a ComposeView, and full Compose screens live inside them. This keeps the back stack and state intact without rewriting navigation all at once.

Hybrid Architecture Patterns

Interfaces as Boundaries

The key to a hybrid architecture is a clear boundary between Compose and legacy views. Wrap access to legacy views behind an interface:

KotlinBatas interop
interface PetaProvider {
    @Composable
    fun Tampilkan()
}
 
class PetaLegacy : PetaProvider {
    @Composable
    override fun Tampilkan() {
        AndroidView(factory = { context -> MapView(context) })
    }
}

PetaLegacy hides the legacy view behind the PetaProvider interface. The rest of the application uses the interface without caring about the implementation — later the legacy view can be replaced without changing callers.

The End Goal

After all screens move, remove the legacy FragmentActivity and make it a Single Activity architecture (episode 8). The intermediate interfaces remain useful as test points and isolation.

Closing

Episode 16 bridged two worlds: ComposeView for embedding Compose into XML, AndroidView for embedding legacy views into Compose, a gradual screen migration strategy that prioritizes dynamic screens, and interfaces as clean hybrid architecture boundaries.

Key takeaways:

  • ComposeView allows Compose to live inside an XML layout.
  • AndroidView embeds legacy views into a composable.
  • Gradual migration starts from the most dynamic screens.
  • Legacy Fragments can wrap ComposeView without overhauling navigation.
  • Interfaces separate Compose from legacy view implementations.
  • The end goal of migration is a Single Activity architecture.

In episode 17 we will discuss desktop, web, and multiplatform — Compose Multiplatform, building and deploying to desktop and web, sharing UI components across targets, and its limitations and practical use cases.

Learn Jetpack Compose - Interop & Migration | Learn Jetpack Compose