AndroidView: Use Legacy Views Inside Jetpack Compose

Quick answer: Use AndroidView when 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 in factory, push Compose state into it in update, 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.

SituationBest starting point
Third-party SDK exposes only an Android ViewAndroidView around that widget.
A small legacy XML layout already has View BindingAndroidViewBinding for that contained layout.
A custom View is simple and actively maintainedPlan a Compose replacement, beginning with the simplest one.
A whole View-based screen must temporarily host new Compose contentComposeView in the existing XML host.
A legacy Fragment is needed during migrationUse 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:

  • factory creates the View once on the UI thread and performs one-time setup, such as installing listeners or constant configuration.
  • update runs 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 dataUsually owns itBridge behavior
Screen data, selection, loading, errors, form submissionViewModel or another Compose state holderRead it in update; send View events back through callbacks.
Temporary internal View mechanics, such as an animation frameThe ViewDo not mirror every internal value into Compose.
Durable SDK state, such as web navigation historyThe SDK View plus an explicit save/restore planFollow 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 onRelease when 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.