Skip to content
AKarda-kazancimainbrew()
Personal blog
AK
arda-kazanci › journal ›decoupling-webview-state-from-activity-lifecycles-with-webcontent.md
← Journal.md
decoupling-webview-state-from-activity-lifecycles-with-webcontent.md134 lines · Markdown · UTF-8
Markdown Preview · decoupling-webview-state-from-activity-lifecycles-with-webcontent.md Rendered
journal/decoupling-webview-state-from-activity-lifecycles-with-webcontent.md

Decoupling WebView State from Activity Lifecycles with WebContent

How the new androidx.web library separates the underlying Chromium session from the view hierarchy to preserve DOM, JavaScript heap, and scroll state across configuration changes.

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:

  1. WebContent: An engine controller that implements AutoCloseable. It maintains the page URL, JavaScript state, DOM tree, WebViewClient, and WebChromeClient. It does not draw pixels and has no view hierarchy constraints.
  2. WebContentView: A direct subclass of android.webkit.WebView. It acts as the actual on-screen view, bound to an active Activity context.
  3. DetachedWebContentView: A specialized, non-renderable WebContentView created internally when detach() is called. When detached, Chromium routes engine callbacks (such as onPageFinished) to this fallback view so they never deliver a null view or leak the dead Activity. If this view is ever added to a view hierarchy, its onAttachedToWindow callback throws an IllegalStateException.

Safety checks and runtime requirements

The Chromium support layer enforces strict preconditions:

  • Factory enforcement: The attach method accepts an Activity context 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 active WebContentView to already be removed from its parent view group (view.parent == null). Attempting to detach an attached view throws an IllegalStateException.
  • Feature verification: The feature requires support in the device’s installed WebView APK. You must check WebFeature.isFeatureSupported(WebFeature.WEB_CONTENT) before calling any WebContent builder method.

Implementation with Jetpack Compose

Add the dependency to your version catalog:

gradle/libs.versions.toml
[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.

WebContentViewModel.kt
package com.example.androidxnews.webcontent
import androidx.lifecycle.ViewModel
import androidx.web.WebContent
import 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().

WebContentScreen.kt
package com.example.androidxnews.webcontent
import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.web.WebContentView
@Composable
fun 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 View system rather than Chromium’s renderer. When switching views, WebContentView.transferViewState reads scrollX and scrollY from the departing view and applies them via scrollTo to the new view.
  • Resource cleanup: WebContent keeps memory allocated in the browser engine process until close() is explicitly invoked. Scoping its lifecycle to a ViewModel ensures close() runs when the user permanently leaves the flow.
SHARE ENTRYLinkedIn ↗X ↗Markdown / AI ↗
Discussions ardakazanci/blog-comments
● GiscusView discussions on GitHub ↗
⑂Reactions & comments/ Sign in with GitHub to react or join the discussion.
Notifications Newsletter · beehiiv
✓ Active channelSubscribe directly ↗
✉Get new posts by email/ Android, Kotlin, and Compose notes. Straight to your inbox.

SUBSCRIPTION CONFIG · EMAIL DISPATCH

Stay synced with new entries

Receive practical engineering notes on Android architecture, Kotlin, and Jetpack Compose whenever a new article is published.

channel = "email"spam = falseunsubscribe = "anytime"
K subscriber.propertiesEmail address