When migrating an Android application to Jetpack Compose, it is often necessary to reuse existing XML layouts or custom Views within the new declarative UI. The AndroidView composable facilitates this interoperability by allowing developers to embed standard Android Views directly into a Compose hierarchy.
Previewing Legacy Layouts
To preview an XML layout within a Composable function, you can use the @Preview annotation alongside AndroidView. In the following example, a legacy layout resource is inflated and configured to fill the maximum available space.
@Preview
@Composable
fun LegacyViewPreview() {
AndroidView(
factory = { context ->
LayoutInflater.from(context).inflate(R.layout.item_details_layout, null)
},
modifier = Modifier.fillMaxSize()
)
}
The factory lambda is responsible for creating the View instance. Once the View is created, you can perform further updates using the update parameter. This callback receives the inflated View, allowing you to modify its properties or attach listeners as the Compose state changes.
AndroidView(
factory = { context ->
LayoutInflater.from(context).inflate(R.layout.item_details_layout, null)
},
update = { view ->
// Logic to update the View state
val textView = view.findViewById<TextView>(R.id.statusText)
textView.text = "Updated State"
}
)
Embedding Compose in Fragments
Conversely, you may need to introduce Compose UI elements into a traditional Fragment-based architecture. This is achieved by adding a ComposeView to the Fragment's view hierarchy. When dealing with multiple ComposeView instances within a single layout, it is critical to assign unique resource IDs to them.
If unique IDs are not assigned, the system may fail to correctly manage the view lifecycle or restore state, potentially leading to crashes when navigating away from and back to the screen. You should define these IDs in a resource file (e.g., res/values/ids.xml).
<resources>
<item name="dashboard_compose_a" type="id" />
<item name="dashboard_compose_b" type="id" />
</resources>
In the Fragment implementation, you can programmatically add these views. Its best practice too set the ViewCompositionStrategy to DisposeOnViewTreeLifecycleDestroyed to ensure the Compose content is properly disposed of when the Fragment's view is destroyed.
class DashboardFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
addView(
createComposeView(R.id.dashboard_compose_a)
)
addView(TextView(requireContext()).apply {
text = "Legacy Header"
})
addView(
createComposeView(R.id.dashboard_compose_b)
)
}
private fun createComposeView(@IdRes id: Int): ComposeView {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
this.id = id
// Set content here
}
}
}
Context Handling Differences
It is important to distinguish between how Context is accessed depending on whether you are working in the View system or Compose.
- Inside a Fragment (View System): Use the Fragment's method
requireContext()to access the context associated with the host Activity. - Inside a Composable: Use the
LocalContextcomposable local to retrieve the current Context.
// Inside a Composable function
@Composable
fun MyComposableScreen() {
val context = LocalContext.current
// Use context as needed
}