ComposeView is easy; deciding where to put it is the migration
Adding Compose to a View-based app is one class. The interesting decisions are which seam to cut at, and the ViewCompositionStrategy that governs when the composition is disposed.

Day 88 of 100, opening the interop pillar. Almost nobody starts a Compose migration on a blank project, and the mechanics are the easy part.
The symptom
Compose added to a RecyclerView item, and the app starts leaking:
class OrderViewHolder(private val composeView: ComposeView) :
RecyclerView.ViewHolder(composeView) {
fun bind(order: Order) {
composeView.setContent { OrderRow(order) }
}
}
Memory climbs as the list scrolls. Compositions from recycled view holders are still
alive, still subscribed to state, still holding references to Order objects that should
have been collected.
Nothing errors. The list works. The heap grows.
Why the obvious fix fails
The obvious fix is to clear the content on recycle:
override fun onViewRecycled(holder: OrderViewHolder) {
holder.composeView.setContent { } // "clear it"
}
This replaces the composition rather than disposing it, so the machinery stays alive. It also runs a composition to render nothing, which is work for no output.
The real issue is that ComposeView doesn't know when it should tear down, and by default
it makes an assumption that's wrong for recycled views.

The actual mechanism
ComposeView is a View that hosts a composition. Its lifetime is governed by
ViewCompositionStrategy, and the default is
DisposeOnDetachedFromWindowOrReleasedFromPool — dispose when the view detaches from the
window, or when a pool releases it.
That default is right for most cases and needs help in a RecyclerView, where the correct
strategy is tied to the view tree lifecycle:
class OrderViewHolder(private val composeView: ComposeView) :
RecyclerView.ViewHolder(composeView) {
init {
composeView.setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
}
fun bind(order: Order) {
composeView.setContent { OrderRow(order) }
}
}
The four strategies, and when each is right:
DisposeOnDetachedFromWindowOrReleasedFromPool— the default. Correct for aComposeViewin a normal layout.DisposeOnViewTreeLifecycleDestroyed— dispose when the owning lifecycle is destroyed. Right for fragments and pooled views, where detach doesn't mean "gone".DisposeOnLifecycleDestroyed(lifecycle)— the explicit form, when you know the lifecycle.DisposeOnDetachedFromWindow— the older, more aggressive form. Rarely correct now.
Getting this wrong produces either a leak (disposed too late) or a composition torn down while still visible (disposed too early). Neither errors.
The three places Compose goes in
Mechanically there are three seams, in increasing order of commitment:
1. A ComposeView in an XML layout.
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_summary"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
binding.composeSummary.setContent { AppTheme { OrderSummary(state) } }
Lowest commitment, and right for adding one component to an existing screen.
2. A whole fragment's content.
override fun onCreateView(…): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { AppTheme { OrderScreen(viewModel) } }
}
The most common migration unit, because a fragment is a natural boundary — its navigation, its ViewModel scope and its lifecycle all stay unchanged.
3. A whole activity.
setContent { AppTheme { App() } }
The end state. Worth noting that setContent on an Activity is ComponentActivity's,
and it manages the strategy for you.
Choosing the seam
The mechanics above are a day's work. Which seam to cut at is the migration, and three rules cover most of it:
Migrate a screen at a time, not a component at a time. A screen that's half Compose and half XML has two theming systems, two state models and two sets of tests. A whole fragment converted is a clean unit with an unchanged interface to the rest of the app.
Start with a leaf. A screen nothing else depends on — settings, an about page, a detail view. It exercises the theming, the ViewModel wiring and the test setup without risking a central flow.
Don't migrate what you're about to delete. Obvious, and routinely ignored. A screen scheduled for redesign is better rewritten in Compose as part of the redesign than converted twice.
The things that need bridging
Four, and they're the ones people hit in the first week:
Theming. A Compose MaterialTheme doesn't read your XML theme. MdcTheme /
AppCompatTheme from the accompanist-era libraries bridged this; the durable answer is to
define the Compose theme (Day 44) from the same design tokens, which is work you'd do
anyway.
ViewModels. These work unchanged — viewModel() in a fragment's ComposeView
resolves the fragment's own ViewModel scope, which is one of the reasons the fragment seam
is comfortable.
Resources. stringResource, dimensionResource and colorResource read your existing
XML resources, so strings and dimensions don't need migrating.
Insets. A View-based app usually handles insets in XML with fitsSystemWindows. Compose
handles them with WindowInsets (Day 35). A screen with both fights itself, which shows
up as double padding under the status bar.
Testing across the seam
Espresso and Compose tests interoperate — a createAndroidComposeRule gives both:
@Test fun mixedScreen() {
onView(withId(R.id.legacy_toolbar)).check(matches(isDisplayed())) // Espresso
composeRule.onNodeWithText("Submit").performClick() // Compose
}
Both consult the same idling registry (Day 86), so synchronisation works across the boundary. This is worth knowing early — it means a half-migrated screen is still testable end to end, which removes an argument against migrating incrementally.
How to prove it
The leak is directly measurable, and worth checking on the first ComposeView you add to
a recycled container:
Scroll a list of a hundred items, then trigger a GC in the memory profiler and look for
retained Composition instances. With the wrong strategy the count climbs with the scroll;
with DisposeOnViewTreeLifecycleDestroyed it stays flat.
LeakCanary catches the same thing without the profiler, and is the cheaper permanent answer.
The migration order that works
One sequencing note, because it's the question every team asks second.
Establish the theme first, in its own change. Everything migrated afterwards renders correctly from the start, and a half-migrated screen doesn't need a temporary bridge that later has to be removed.
Then the design-system components — your AppButton, AppCard, AppTextField from
Day 48 — as Compose composables. These are used by every screen, so having them ready
means each screen migration is assembly rather than invention.
Then screens, leaf-first.
Teams that go straight to screens end up writing the theme and the components three times, slightly differently, and reconciling them later.
What this generalizes to
The principle is a hosted runtime needs to be told when to stop. ComposeView embeds a
whole composition inside a View, and the View system's lifecycle vocabulary — attached,
detached, recycled — doesn't map one-to-one onto "this composition should be disposed".
The strategy parameter is where you supply the missing information.
Any time one framework hosts another, that mismatch is where the bugs are: a WebView in a fragment, a media player in a view holder, a coroutine scope in a custom view. The question to ask on day one is what tells this thing it's finished, and the answer is rarely the default.
The migration advice generalises too, and it's the less technical half. Cutting at a seam the existing architecture already recognises — a fragment, a screen, a feature module — means the rest of the app doesn't need to know a migration is happening. Cutting at a seam you invent for the migration means every boundary is new and every one can break.
Tomorrow, Day 89: the other direction — putting Views inside Compose, and the update lambda that makes it work.
Day 88 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Compose in Views.