AndroidView: Use Legacy Views Inside Jetpack Compose

Quick answer: Use
AndroidViewwhen a Compose screen needs a View-based SDK widget, a platform component without a Compose equivalent, or a legacy custom view you cannot replace yet. Create the View once infactory, push Compose state into it inupdate, send View events back through callbacks, and release external resources when the View permanently leaves composition.
AndroidView is the supported interoperability boundary in the View-inside-Compose direction. It lets a Compose screen keep its declarative state and layout while hosting a focused View dependency such as an AdView, SurfaceView, WebView, map widget, or a mature custom View.
It is a bridge, not a second UI architecture. Use it for the narrow piece that must remain a View, then keep the surrounding feature in Compose.
Decide whether a View should stay
The platform guidance recommends AndroidView for a component that does not have an appropriate Compose API—not as the default way to carry an entire legacy screen forward.
| Situation | Best starting point |
|---|---|
Third-party SDK exposes only an Android View | AndroidView around that widget. |
| A small legacy XML layout already has View Binding | AndroidViewBinding for that contained layout. |
| A custom View is simple and actively maintained | Plan a Compose replacement, beginning with the simplest one. |
| A whole View-based screen must temporarily host new Compose content | ComposeView in the existing XML host. |
| A legacy Fragment is needed during migration | Use the dedicated AndroidFragment API, not a hand-built FragmentContainerView in AndroidView. |
For the reverse direction—adding Compose to an XML screen—see ComposeView: Add Compose to an Existing XML Screen. Keeping the two directions distinct prevents deep View → Compose → View nesting that becomes difficult to test and eventually remove.
The factory / update contract
AndroidView has two responsibilities that must remain separate:
factorycreates the View once on the UI thread and performs one-time setup, such as installing listeners or constant configuration.updateruns after creation and can run again as Compose state read inside it changes. Use it to synchronize the current state into the existing View.
This example adapts a hypothetical legacy rating widget while keeping the screen state outside the View:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun LegacyRating(
rating: Int,
onRatingChange: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val currentOnRatingChange by rememberUpdatedState(onRatingChange)
AndroidView(
modifier = modifier,
factory = { context ->
LegacyRatingView(context).apply {
setOnRatingChangedListener { newRating ->
currentOnRatingChange(newRating)
}
}
},
update = { view ->
if (view.rating != rating) {
view.rating = rating
}
},
)
}The listener reports a user event to Compose; the owner updates rating; Compose recomposes; and update synchronizes the View. That is the same state-down, events-up model used by ordinary composables.
rememberUpdatedState matters when the caller supplies a new callback after recomposition. The View is created only once, so a listener installed in factory should read the latest callback rather than capture an obsolete lambda. Do not create the View in remember outside AndroidView; the AndroidView documentation explicitly recommends the factory lambda as the creation owner.
Keep View state and Compose state from fighting
For every property, decide which layer is authoritative:
| Kind of data | Usually owns it | Bridge behavior |
|---|---|---|
| Screen data, selection, loading, errors, form submission | ViewModel or another Compose state holder | Read it in update; send View events back through callbacks. |
| Temporary internal View mechanics, such as an animation frame | The View | Do not mirror every internal value into Compose. |
| Durable SDK state, such as web navigation history | The SDK View plus an explicit save/restore plan | Follow the SDK lifecycle requirements; release resources on final removal. |
The mistake is to let both layers mutate the same fact independently. For example, do not keep a remember { mutableStateOf(...) } value for a selected item while a legacy widget changes selection internally without reporting it. Give the widget a callback, update the state owner, and render the resulting state back into the View.
State Hoisting in Jetpack Compose: Real Examples is the useful companion when deciding whether a value belongs in a reusable wrapper, a screen state holder, or a ViewModel.
Use AndroidViewBinding for a small XML hierarchy
When the dependency is a small existing XML layout rather than one View, AndroidViewBinding can be clearer than manually inflating the binding in factory. It is supplied by the androidx.compose.ui:ui-viewbinding artifact, and the Android project must have View Binding enabled.
import androidx.compose.runtime.Composable
import androidx.compose.ui.viewinterop.AndroidViewBinding
@Composable
fun LegacyStatusPanel(
message: String,
isError: Boolean,
) {
AndroidViewBinding(LegacyStatusPanelBinding::inflate) {
statusMessage.text = message
statusIcon.isSelected = isError
}
}Use this for an intentionally contained legacy panel. It is not a reason to inflate a full screen-level XML layout inside a Compose-only app. Android’s Views-in-Compose guide recommends limiting AndroidViewBinding to smaller legacy layouts during incremental migration.
Reuse Views deliberately in Lazy layouts
By default, AndroidView does not pool or reuse View instances. In a LazyColumn, LazyRow, or pager, that can mean repeated allocation as items enter and leave the composition. The reusable overload enables reuse when you supply a non-null onReset callback.
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
@Composable
fun LegacyPreviewRow(
item: PreviewUi,
modifier: Modifier = Modifier,
) {
AndroidView(
modifier = modifier.fillMaxWidth(),
factory = { context -> LegacyPreviewView(context) },
update = { view ->
view.bind(item)
},
onReset = { view ->
view.clearTransientState()
},
onRelease = { view ->
view.release()
},
)
}onReset prepares a View for a different item: clear animations, input, selection, listeners tied to the old item, and temporary content. Then update binds the current item. onRelease is for resources that are permanently finished, such as a player, renderer, or SDK session. It is not the same as onReset; once onRelease returns, Compose will not reuse that instance.
The AndroidView API reference notes one important edge case: onReset is not guaranteed to be followed immediately by update. Reset the View to a safe neutral state, and do not assume it is immediately visible with a new item. Use stable lazy-list keys as well so item identity remains clear at the Compose level.
Respect the embedded View’s lifecycle
AndroidView owns insertion and removal from the Compose hierarchy. It does not erase lifecycle requirements of a complex View or SDK.
- Create the View and its one-time listeners in
factory. - Update current content and state in
update. - Release SDK resources in
onReleasewhen they should not survive a permanent exit. - Let lifecycle-aware Views attach to the View tree before relying on
findViewTreeLifecycleOwner(). - Follow the SDK’s own guidance for pause, resume, save, restore, and destruction.
WebView is an especially stateful example. Its current Compose guidance warns that WebView has state lifecycles separate from both Compose and the Android View lifecycle. Do not keep a WebView instance alive in remember across Activity lifecycles; save only the state the integration supports and clean it up when leaving composition.
Preserve Compose layout and accessibility semantics
AndroidView accepts a Modifier, so Compose remains responsible for its placement, size, padding, click boundaries, and parent scrolling behavior. Give the wrapper a clear size contract and avoid two layers both trying to own the same scroll gesture.
The embedded View retains its own accessibility semantics. Test the complete screen with TalkBack and keyboard navigation rather than assuming the Compose parent fills in missing labels or roles. If the View is only decorative, mark it as such through the appropriate View API; if it is interactive, verify its focus order, labels, and state announcements alongside nearby Compose controls.
For testing, keep the wrapper composable small enough to use a fake or test-friendly View implementation when practical. Then add an instrumented test for the real SDK View, because Compose semantics tests cannot inspect every behavior that exists solely inside the View hierarchy.
Common mistakes
Building the View in remember
This separates creation from the interoperability lifecycle and can retain an Activity or stale resources longer than intended. Let factory create the View.
Putting state synchronization in factory
factory is one-time setup. A title, selected value, error flag, URL, or current model can change, so synchronize it in update.
Using a View wrapper when Compose already has the component
An extra boundary adds lifecycle, accessibility, and test work. Prefer a native Compose component when it meets the need, and reserve AndroidView for a genuine gap or a staged migration.
Reusing a pooled View without resetting it
Without a complete onReset, the next lazy-list item can inherit focus, listeners, a running animation, or content from the previous item. Reset transient state before binding the new item.
Hosting a whole app screen through AndroidViewBinding
That freezes a View hierarchy inside a Compose shell instead of advancing the migration. Keep the XML boundary small and make a plan to replace it when Compose can own the feature.
A practical migration rule
Use AndroidView to contain an unavoidable View dependency, not to spread View state through a Compose screen. Keep the bridge small, let one state owner drive updates, test lifecycle and accessibility at the boundary, and reuse or release the View only with a clear contract.
For the migration sequence that decides which View to wrap, replace, or leave alone, see Android View to Compose Migration: Complete Developer Guide. The result is a Compose screen that can ship today while its remaining legacy dependency stays visible, isolated, and removable.