Skip to main content
This guide covers how to embed the bridge-connected WebView in your Android application, including Jetpack Compose and View-system integration patterns.

Jetpack Compose Integration

Unlike iOS, there is no prebuilt BridgeWebView composable. The Android pattern is three steps: construct a WebView inside an AndroidView factory, call host.attach(webView), then loadUrl(...). attach() configures the WebView internally (JavaScript, DOM storage, the bootstrap-injecting WebViewClient), so there is no separate setup call.

Attaching a WebView in Compose

Hold the BridgeHost in remember { } so it survives recomposition, attach it in the AndroidView factory, and detach in DisposableEffect.onDispose.
attach() runs BridgeWebViewConfigurator.configure() internally. Never call configure() yourself before attach() β€” attach() would simply re-run it and clobber whatever you set up.

Observing Bridge State

BridgeHost is not observable β€” there is no ObservableObject analogue, and reading properties like lastError does not trigger recomposition. To drive Compose UI from bridge activity, mirror state into Compose via a BridgeHostDelegate. (The typed capture slots are the exception: host.documentCapture.activeRequest is a StateFlow, so you can collectAsState() it directly.)
BridgeHost.delegate is stored as a WeakReference β€” the host does not keep your delegate alive. An inline host.delegate = SomeDelegate() with no other strong reference silently stops firing after the next garbage collection. Hold the delegate as a property of a remembered object (as JourneyBridge does above) so it stays pinned for the lifetime of the screen.

Passing the Host to Child Views

BridgeHost is a plain class, so pass it to child composables as an ordinary parameter β€” no property-wrapper ceremony is needed. The flip side: properties like receivedMessages are immutable snapshots per read and won’t recompose your UI when they change, so mirror anything you want to display through the delegate. When rendering message lists, key items by correlationId (BridgeMessage has no id property on Android).

Dynamic URL Changes

There is no URL binding to update. The AndroidView factory runs once, so navigate to a new URL from the update block (which runs on each recomposition) by calling loadUrl(...) when your URL state changes. The bridge survives navigation β€” the bootstrap script is re-injected on every page load.

View System Integration

For Activity- or Fragment-based apps, the pattern is identical β€” only the WebView construction differs. There is no makeWebView factory on Android: create the WebView yourself (programmatically or inflated from XML), then call host.attach(webView) and loadUrl(...).

Attaching in an Activity

Hold the host as a property so it lives as long as the Activity, attach in onCreate, and tear down in onDestroy.

What attach() Configures

attach() calls BridgeWebViewConfigurator.configure() internally: it enables javaScriptEnabled and domStorageEnabled, installs the bootstrap-injecting WebViewClient, and installs a plain WebChromeClient β€” unconditionally overwriting any clients already set on the WebView. Three practical consequences:
  1. Don’t call configure() separately before attach() β€” it would be re-run and clobbered by attach()’s own configure pass.
  2. Set a custom WebChromeClient after attach(), never before, or the plain one installed by configure() will overwrite yours.
  3. Other WebView settings are untouched. If you need additional settings (media playback, caching, etc.), apply them on the WebView yourself β€” attach() won’t reset them.

Lifecycle Considerations

The BridgeHost and the WebView have separate lifecycles, the delegate is weakly held, and handlers should be set up before content loads. Keep these in mind to avoid leaks, silent callback loss, and missed messages.

Host Lifetime

The BridgeHost should live at least as long as the WebView. In Compose, use remember { } (scoped to the screen) so the host isn’t recreated on recomposition. In the View system, hold a strong reference as an Activity property or in a ViewModel. Whatever owns the host should also strongly hold the delegate and any handler objects, since the host won’t keep them alive.

WebView Detachment

Calling detach() removes the JavaScript interface, cancels any in-flight typed-slot capture (via cancelIfBusy), and clears pendingRequests and receivedMessages β€” unlike iOS, which preserves the message buffers across detach. Clearing avoids ghost entries in Compose lists when you re-attach. detach() is idempotent and safe to call from onDispose. If you call respond(...) or sendEvent(...) while no WebView is attached, nothing throws: the host fires delegate.onMessageSent (so you can trace the intent) and then silently drops the message at the transport. This diverges from iOS, which records lastError = "WebView not attached" β€” on Android no lastError is set for this case. If you registered raw BridgeCapabilityHandlers that hold on to a pending BridgeResponder, cancel or respond to them yourself before detaching so the web journey isn’t left waiting on a response that will never arrive:

Re-attaching a WebView

If you need to replace the WebView (e.g., after a navigation reset), call detach() and then attach() with the new WebView. Inbound messages still in flight from the old attach session are identity-gated and dropped, so the new session starts clean.

Disposing the Host

dispose() is an Android-only addition (iOS relies on ARC for teardown). It performs a terminal teardown: detach plus cancellation of the typed capture slots’ coroutine scopes. After dispose(), state-mutating methods (attach, register, respond, sendEvent, and friends) throw IllegalStateException; detach(), dispose(), clearError(), the getters, and the delegate setter remain safe. Call it from a terminal lifecycle callback β€” Activity.onDestroy or ViewModel.onCleared β€” wrapped in try/catch with a log, since removeJavascriptInterface can throw on a WebView that is already shutting down.
Use detach() for a recoverable teardown (the screen may come back and re-attach) and dispose() only when the host will never be used again.

Setting Up Handlers

Set handlers on typed slots or register custom capabilities before the WebView loads content. If the web journey sends a request before a handler is available, the request goes to pendingRequests (and delegate.onUnhandledRequest fires) β€” you can still answer it later via the lookup overload of respond(...).

Custom WebViewClient

On Android, navigation policy lives in the WebViewClient β€” and the bridge depends on the one attach() installs, because it injects the bootstrap script on every page load. If you need custom navigation behavior (intercepting links, logging load errors, SSL handling), don’t replace the client wholesale: subclass BridgeWebViewConfigurator.BootstrapInjectingWebViewClient and pass your instance to attach(webView, client = ...). Override whatever callbacks you need; if you override onPageStarted, call super.onPageStarted(...) to keep the bootstrap injection. Two things to know when supplying your own client:
  • The bootstrapScript parameter on configure() is ignored when a client is passed β€” your subclass owns its script, so pass the script to the subclass constructor. The SDK’s default bootstrap literal is internal, so supply it yourself.
  • The bootstrap injects on the main frame only, on every page load.
Bootstrap timing diverges from iOS. iOS injects the bootstrap via WKUserScript at .atDocumentStart, guaranteed to run before any page JavaScript. Android injects it via evaluateJavascript from onPageStarted, which is best-effort: a <script> in the document head that synchronously calls window.GBGBridge.receive could race and find it undefined. Web code should not assume the bootstrap is present before its own head scripts run.
To debug a blank or broken journey page, call WebView.setWebContentsDebuggingEnabled(true) on debuggable builds (gate it on ApplicationInfo.FLAG_DEBUGGABLE so release builds never opt in) and inspect the WebView with full DevTools at chrome://inspect. Setting a WebChromeClient that overrides onConsoleMessage β€” after attach() β€” forwards the page’s console.* output to logcat.

Next Steps