ComposeView: Add Compose to an Existing XML Screen

Quick answer: Add a
ComposeViewto the existing XML layout, find it from the Fragment or Activity, choose a disposal strategy that matches the host lifecycle, and callsetContent { ... }. Keep one owner for screen state—usually the existingViewModel—and replace a coherent section of the screen rather than nesting both UI toolkits throughout the same feature.
ComposeView is Android’s supported bridge for rendering Compose inside an existing View hierarchy. It lets a View-based screen keep its Fragment, navigation, toolbar, and surrounding XML while a new card, form section, or feature area is written in Compose. That makes incremental migration a delivery strategy, not a rewrite project.
Choose a boundary that can stand on its own
The best first ComposeView is usually a meaningful section with a small surface area:
| Good first boundary | Why it works |
|---|---|
| A new summary card or empty state | It has a contained layout and clear state input. |
| A settings group or reusable control | It can become a reusable composable without changing the whole screen. |
| A section already being redesigned | The product work gives the migration a concrete purpose. |
| A full screen whose Fragment host must remain temporarily | The Fragment can return or contain one ComposeView while navigation is still View-based. |
Avoid converting one row, then putting an AndroidView inside it, then nesting more Views inside that. A clear View-to-Compose boundary is easier to theme, test, and eventually remove. The broader trade-offs are covered in Jetpack Compose vs XML Views: Which Should You Use?; this article focuses on the concrete Compose-in-Views direction.
Add ComposeView to the XML host
Place ComposeView where the Compose-owned portion of the screen should appear. It is a normal Android View, so it participates in the existing layout, constraints, scrolling, and view binding.
<!-- res/layout/fragment_article.xml -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/legacy_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/article_summary_compose"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/legacy_title" />
</androidx.constraintlayout.widget.ConstraintLayout>Use constraints or layout parameters that communicate the intended size. A ComposeView with wrap_content is suitable for a measured card or section; use a constrained or match_parent dimension when the Compose content is meant to own remaining screen space. Do not hide an unresolved size contract with arbitrary fixed heights.
If the project uses View Binding, the generated binding gives a type-safe reference to the ComposeView. The current Compose-in-Views guide shows both findViewById and View Binding approaches.
Set the composition lifecycle in a Fragment
Composition is stateful. When the ComposeView leaves the screen, Compose needs to know when its composition should be disposed. The right answer depends on the host.
For a ComposeView in a Fragment’s view hierarchy, explicitly use DisposeOnViewTreeLifecycleDestroyed. It ties the composition to the lifecycle owner of the Fragment view, avoiding stale UI after onDestroyView() while the Fragment instance itself remains on the back stack.
import android.os.Bundle
import android.view.View
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.Fragment
class ArticleFragment : Fragment(R.layout.fragment_article) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val composeView = view.findViewById<ComposeView>(
R.id.article_summary_compose,
)
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed,
)
composeView.setContent {
AppTheme {
ArticleSummary(
article = sampleArticle,
onOpen = ::openArticle,
)
}
}
}
private fun openArticle() {
// Delegate to the existing navigation owner.
}
}The Android documentation distinguishes this from the default strategy. ViewCompositionStrategy.Default is appropriate for a non-Fragment mixed screen and understands pooling containers such as RecyclerView; a Fragment view needs a view-lifecycle-aware strategy. Review the official strategy table whenever the host is not a straightforward Fragment or Activity.
Keep state in one place
Adding Compose should not create a second UI state system. If the existing Fragment uses a ViewModel, continue to make it the owner of screen state and events. Compose reads the same state and calls the same actions; the remaining Views observe or render that same source of truth.
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun ArticleSummaryHost(
viewModel: ArticleViewModel,
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
ArticleSummary(
article = state.article,
isSaved = state.isSaved,
onOpen = { viewModel.onAction(ArticleAction.Open) },
onSave = { viewModel.onAction(ArticleAction.Save) },
)
}The bridge then stays thin:
composeView.setContent {
AppTheme {
ArticleSummaryHost(viewModel)
}
}This avoids a common migration bug: a legacy listener changes a View while Compose holds a separate remember value for the same fact. State can be local to the composable when it is purely transient—such as whether an internal tooltip is expanded—but selected items, loaded data, form values, and save state should have one owner. State Hoisting in Jetpack Compose: Real Examples explains how to separate state, events, and reusable UI.
collectAsStateWithLifecycle() comes from lifecycle-runtime-compose. If the screen already collects state another way, preserve the established lifecycle-aware pattern rather than adding two collectors for the same stream.
Apply the Compose theme intentionally
ComposeView does not automatically make a View theme and a Compose MaterialTheme identical. Wrap the content in the app’s Compose theme and compare its typography, colors, shapes, density, and insets with the View screen around it.
composeView.setContent {
AppTheme {
ArticleSummaryHost(viewModel)
}
}This wrapper belongs at the boundary so the new composable can use semantic Material colors and typography without reaching back into Fragment code. Do not copy a theme into every small composable; that makes it harder to reuse the component later in a Compose screen.
The first boundary is also a good place to preserve accessibility parity: keep labels and descriptions localized, verify focus order across the View/Compose transition, and avoid turning one visible section into two duplicate accessibility stops.
Handle saved state and multiple ComposeViews
One ComposeView needs an Android ID in XML. If the same layout contains multiple ComposeView instances, every one needs a unique ID for savedInstanceState to work correctly. This is easy to miss when views are created programmatically; define stable IDs in res/values/ids.xml when needed.
<!-- res/values/ids.xml -->
<resources>
<item name="compose_header" type="id" />
<item name="compose_summary" type="id" />
</resources>Use rememberSaveable inside a composition only for small UI state that should survive recreation, such as a selected tab or an expanded section. It is not a replacement for repository data or a ViewModel. The Compose-in-Views documentation calls out the unique-ID requirement explicitly.
Preview and test the mixed screen
Compose previews are still useful for the new component, but they do not show every constraint, inset, or nested-scroll behavior from the XML host. Use both levels of feedback:
- Add a
@Previewfor the standalone composable with representative loading, long-text, and error states. - Use Layout Editor’s
tools:composableNameon the XMLComposeViewwhen a mixed-layout preview is helpful. - Run the Fragment or Activity and test scrolling, focus traversal, TalkBack, configuration changes, and Back navigation across the boundary.
- Keep existing View tests for the host behavior, and add Compose UI tests for the new composable’s semantics and interactions.
The official guide documents tools:composableName for seeing a preview composable inside a layout that contains ComposeView. For broader screen-level test patterns, the existing Android View to Compose Migration guide provides migration planning context; use the current platform docs for the final lifecycle details.
Common pitfalls
Using the default disposal strategy in a Fragment without checking the host
The default is intentionally suited to common non-Fragment and pooling-container cases. A Fragment view has a separate lifecycle, so make the disposal strategy explicit and test a navigate-away/navigate-back path.
Passing callbacks but duplicating the state
Compose callbacks should drive the same state holder the existing Views use. Do not update a View directly and separately update a Compose-only remember flag for the same action.
Wrapping a whole legacy screen without a migration plan
ComposeView is an excellent boundary for a new section or a deliberately Compose-owned screen. It is not a reason to place a permanent Compose shell around a dense legacy tree. Decide what will remain View-based, what will migrate next, and where the boundary can eventually disappear.
Forgetting scroll and inset ownership
Only one layer should own a scrolling container or a system inset at a given edge. A nested LazyColumn inside an existing RecyclerView or nested scroll host needs a deliberate interaction design, not trial-and-error padding.
A safe incremental path
Start with one contained feature, render it through a lifecycle-safe ComposeView, and keep state and navigation at their existing owners. Validate it beside the legacy UI. Then reuse the composable in the next new screen or replace the adjacent View section when there is a product reason to do so.
That approach gives a View-based app the benefits of Compose immediately while keeping migration scope, lifecycle behavior, and test ownership understandable.