Android developers handling in-app web browsing eventually run into the same architectural limitation: android.webkit.WebView combines an Android View and an active Chromium browser engine into a single class.
Because it inherits directly from View, retaining an instance across configuration changes in a ViewModel holds an indirect reference to an Activity context, causing a memory leak. Conversely, destroying and recreating the view on rotation resets the page. Unlike serializable UI fields persisted in a SavedStateHandle, WebView.saveState only serializes the navigation back-forward history; it cannot save the in-memory JavaScript heap, ongoing network requests, form inputs, or active DOM modifications.
On November 4, 2026, Google released androidx.web:web:1.0.0-alpha01, introducing the WebContent API to decouple the engine session from the UI tree.
Core components
The library splits the traditional responsibilities of WebView into three primary classes:
WebContent: An engine controller that implementsAutoCloseable. It maintains the page URL, JavaScript state, DOM tree,WebViewClient, andWebChromeClient. It does not draw pixels and has no view hierarchy constraints.WebContentView: A direct subclass ofandroid.webkit.WebView. It acts as the actual on-screen view, bound to an activeActivitycontext.DetachedWebContentView: A specialized, non-renderableWebContentViewcreated internally whendetach()is called. When detached, Chromium routes engine callbacks (such asonPageFinished) to this fallback view so they never deliver anullview or leak the deadActivity. If this view is ever added to a view hierarchy, itsonAttachedToWindowcallback throws anIllegalStateException.
Safety checks and runtime requirements
The Chromium support layer enforces strict preconditions:
- Factory enforcement: The
attachmethod accepts anActivitycontext and a factory function:content.attach(context, ::WebContentView). Under the hood, Chromium wraps the passed context with an internal safety wrapper before handing it to the factory. If a caller tries to bypass the factory or returns a view constructed with an external context, Chromium throws an exception. - Detached from parent before detach: Calling
webContent.detach()requires the activeWebContentViewto already be removed from its parent view group (view.parent == null). Attempting to detach an attached view throws anIllegalStateException. - Feature verification: The feature requires support in the device’s installed WebView APK. You must check
WebFeature.isFeatureSupported(WebFeature.WEB_CONTENT)before calling anyWebContentbuilder method.
Implementation with Jetpack Compose
Add the dependency to your version catalog:
[versions]web = "1.0.0-alpha01"
[libraries]androidx-web = { group = "androidx.web", name = "web", version.ref = "web" }1. The ViewModel
The ViewModel owns the WebContent session and closes it in onCleared(), without importing Context or any Android View class.
package com.example.androidxnews.webcontent
import androidx.lifecycle.ViewModelimport androidx.web.WebContentimport androidx.web.WebFeature
class WebContentViewModel : ViewModel() {
val webContent: WebContent? = if (WebFeature.isFeatureSupported(WebFeature.WEB_CONTENT)) { WebContent() } else { null }
private var initialLoaded = false
fun markInitialLoadNeeded(): Boolean { if (initialLoaded) return false initialLoaded = true return true }
override fun onCleared() { webContent?.close() }}2. The Composable
AndroidView owns the Context and WebContentView lifecycle in the UI layer. When the device WebView APK lacks WEB_CONTENT support, factory falls back to constructing WebContentView(context) directly. Because AndroidViewHolder runs onRelease before detaching its child from the container, removeView must execute before webContent.detach().
package com.example.androidxnews.webcontent
import android.annotation.SuppressLintimport android.view.ViewGroupimport android.webkit.WebViewClientimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.viewinterop.AndroidViewimport androidx.lifecycle.viewmodel.compose.viewModelimport androidx.web.WebContentView
@Composablefun WebContentScreen( modifier: Modifier = Modifier, viewModel: WebContentViewModel = viewModel(),) { val webContent = viewModel.webContent AndroidView( modifier = modifier.fillMaxSize(), factory = { context -> if (webContent != null) { webContent.attach(context, ::WebContentView).also { view -> if (viewModel.markInitialLoadNeeded()) { configure(view) } } } else { WebContentView(context).also(::configure) } }, onRelease = { view -> (view.parent as? ViewGroup)?.removeView(view) webContent?.detach() }, )}
@SuppressLint("SetJavaScriptEnabled")private fun configure(view: WebContentView) { view.settings.javaScriptEnabled = true view.webViewClient = WebViewClient() view.loadUrl("https://developer.android.com")}State preservation details
When a new WebContentView attaches to an existing WebContent session:
- JavaScript and DOM: Evaluated scripts, modified document elements, and user inputs remain untouched in Chromium’s render process.
- Scroll offset: Scroll positions live in Android’s
Viewsystem rather than Chromium’s renderer. When switching views,WebContentView.transferViewStatereadsscrollXandscrollYfrom the departing view and applies them viascrollToto the new view. - Resource cleanup:
WebContentkeeps memory allocated in the browser engine process untilclose()is explicitly invoked. Scoping its lifecycle to aViewModelensuresclose()runs when the user permanently leaves the flow.