Compose ↔ View Interop — AndroidView and ComposeView
You don't need to rewrite your entire app to use Compose. Modern Android supports gradual migration through interop.
AndroidView: Embedding Views in Compose
Basic Pattern
AndroidView(
factory = { context ->
MapView(context).apply {
// Initialize
}
},
modifier = Modifier.fillMaxSize(),
update = { mapView ->
// Update when recomposed
}
)
Real Example: Google Maps
AndroidView(
factory = { context ->
MapView(context)
},
update = { mapView ->
mapView.getMapAsync { googleMap ->
googleMap.addMarker(
MarkerOptions().position(LatLng(37.7749, -122.4194))
)
}
},
modifier = Modifier.fillMaxSize()
)
WebView
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
loadUrl("https://example.com")
}
}
)
ComposeView: Embedding Compose in Views
In a Fragment
class LegacyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
MyComposeScreen()
}
}
}
}
In XML Layout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbarcompat.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val composeView = findViewById<ComposeView>(R.id.compose_content)
composeView.setContent {
MyComposeContent()
}
}
}
AndroidViewBinding: Type-Safe Access
AndroidView(
factory = { context ->
MapBinding.inflate(LayoutInflater.from(context)).root
},
update = { view ->
val binding = MapBinding.bind(view)
binding.mapView.getMapAsync { /* ... */ }
}
)
State Sharing: View → Compose
class SharedViewModel : ViewModel() {
val location = MutableLiveData<LatLng>()
}
// In Compose Fragment
class MapsComposeFragment : Fragment() {
private val viewModel: SharedViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, ...): View {
return ComposeView(requireContext()).apply {
setContent {
val location by viewModel.location.observeAsState()
ComposeMapScreen(location)
}
}
}
}
// In View Fragment
class MapsViewFragment : Fragment() {
private val viewModel: SharedViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, ...): View {
val binding = MapBinding.inflate(inflater)
binding.mapView.getMapAsync { googleMap ->
googleMap.setOnCameraIdleListener {
viewModel.location.value = googleMap.cameraPosition.target
}
}
return binding.root
}
}
Migration Strategy
- Start with new screens in Compose
-
Add Compose gradual inside existing screens using
ComposeView - Keep shared ViewModel for state
- Use AndroidView sparingly (only for complex views like maps)
- Migrate screens incrementally — don't wait for 100% completion
Summary
- AndroidView: Legacy View → Compose (factory + update pattern)
- ComposeView: Compose → XML layout or Fragment
- State sharing: ViewModel + LiveData/StateFlow
- Migration: Gradual adoption, mixing both frameworks simultaneously
Your app doesn't need to be 100% Compose to benefit. Start interop today.
Interested in mobile app development? Check out 8 Android app templates on Gumroad!
Top comments (0)