> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embedding

> Jetpack Compose and View-system patterns for displaying journey WebViews.

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`.

```kotlin theme={null}
import android.webkit.WebView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.viewinterop.AndroidView
import com.gbg.gbgbridge.core.BridgeHost

@Composable
fun JourneyScreen(journeyUrl: String) {
  val host = remember {
    BridgeHost(hostVersion = "1.0.0").apply {
      documentCapture.handler = { request ->
        // Present your capture UI, then suspend until it calls complete(...)
        documentCapture.awaitCompletion()
      }
    }
  }

  AndroidView(
    factory = { context ->
      WebView(context).apply {
        host.attach(this)
        loadUrl(journeyUrl)
      }
    },
  )

  DisposableEffect(host) {
    onDispose { host.detach() }
  }
}
```

<Warning>
  `attach()` runs `BridgeWebViewConfigurator.configure()` internally. Never call `configure()` yourself before `attach()` — `attach()` would simply re-run it and clobber whatever you set up.
</Warning>

### 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.)

```kotlin expandable theme={null}
class JourneyBridge(hostVersion: String) : BridgeHostDelegate {
  var onErrorChanged: ((String?) -> Unit)? = null

  val host = BridgeHost(hostVersion = hostVersion).also { it.delegate = this }

  override fun onError(host: BridgeHost, error: Throwable) {
    onErrorChanged?.invoke(host.lastError ?: error.message)
  }
}

@Composable
fun JourneyScreen(journeyUrl: String) {
  val bridge = remember { JourneyBridge(hostVersion = "1.0.0") }
  var errorBanner by remember { mutableStateOf<String?>(null) }

  LaunchedEffect(bridge) {
    bridge.onErrorChanged = { errorBanner = it }
  }

  Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
      modifier = Modifier.fillMaxSize(),
      factory = { context ->
        WebView(context).apply {
          bridge.host.attach(this)
          loadUrl(journeyUrl)
        }
      },
    )

    // Show error banner when an error occurs
    errorBanner?.let { error ->
      Text(
        text = error,
        color = Color.White,
        modifier = Modifier
          .align(Alignment.TopCenter)
          .padding(16.dp)
          .background(MaterialTheme.colorScheme.error, MaterialTheme.shapes.small)
          .padding(horizontal = 12.dp, vertical = 8.dp),
      )
    }
  }

  DisposableEffect(bridge) {
    onDispose { bridge.host.detach() }
  }
}
```

<Warning>
  `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 `remember`ed object (as `JourneyBridge` does above) so it stays pinned for the lifetime of the screen.
</Warning>

### 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).

```kotlin expandable theme={null}
class JourneyBridge(hostVersion: String) : BridgeHostDelegate {
  var onMessageCountChanged: ((Int) -> Unit)? = null

  val host = BridgeHost(hostVersion = hostVersion).also { it.delegate = this }

  override fun onMessage(host: BridgeHost, message: BridgeMessage) {
    onMessageCountChanged?.invoke(host.receivedMessages.size)
  }
}

@Composable
fun JourneyContainer() {
  val bridge = remember { JourneyBridge(hostVersion = "1.0.0") }
  var messageCount by remember { mutableStateOf(0) }

  LaunchedEffect(bridge) {
    bridge.onMessageCountChanged = { messageCount = it }
  }

  Scaffold(
    topBar = {
      TopAppBar(
        title = { Text("Journey") },
        actions = { MessageCountBadge(count = messageCount) },
      )
    },
  ) { padding ->
    JourneyWebView(
      host = bridge.host,
      modifier = Modifier.padding(padding),
    )
  }
}

@Composable
fun JourneyWebView(host: BridgeHost, modifier: Modifier = Modifier) {
  AndroidView(
    modifier = modifier,
    factory = { context ->
      WebView(context).apply {
        host.attach(this)
        loadUrl("https://journey.example.com")
      }
    },
  )
}

@Composable
fun MessageCountBadge(count: Int) {
  Text(
    text = "$count",
    color = Color.White,
    style = MaterialTheme.typography.labelSmall,
    modifier = Modifier
      .background(MaterialTheme.colorScheme.primary, CircleShape)
      .padding(6.dp),
  )
}
```

### 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.

```kotlin theme={null}
@Composable
fun DynamicJourney(host: BridgeHost) {
  var journeyUrl by remember { mutableStateOf("https://journey.example.com/step1") }

  Column {
    AndroidView(
      modifier = Modifier.weight(1f),
      factory = { context ->
        WebView(context).apply {
          host.attach(this)
          loadUrl(journeyUrl)
        }
      },
      update = { webView ->
        if (webView.url != journeyUrl) webView.loadUrl(journeyUrl)
      },
    )

    Button(onClick = { journeyUrl = "https://journey.example.com/step2" }) {
      Text("Next Step")
    }
  }
}
```

## 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`.

```kotlin expandable theme={null}
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import com.gbg.gbgbridge.core.BridgeHost

class JourneyActivity : AppCompatActivity() {
  private val host = BridgeHost(hostVersion = "1.0.0")
  private lateinit var webView: WebView

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Programmatic WebView; an XML alternative is
    // findViewById<WebView>(R.id.journey_web_view) after setContentView(R.layout.…)
    webView = WebView(this).apply {
      layoutParams = ViewGroup.LayoutParams(
        ViewGroup.LayoutParams.MATCH_PARENT,
        ViewGroup.LayoutParams.MATCH_PARENT,
      )
    }
    setContentView(webView)

    host.attach(webView)
    webView.loadUrl("https://journey.example.com")
  }

  override fun onDestroy() {
    try {
      host.dispose()
    } catch (e: Exception) {
      // removeJavascriptInterface can throw on a WebView already in shutdown
      Log.w("JourneyActivity", "Bridge teardown failed", e)
    }
    super.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.

```kotlin theme={null}
host.attach(webView)

// Custom WebChromeClient must come AFTER attach()
webView.webChromeClient = MyConsoleLoggingChromeClient()

webView.loadUrl(journeyUrl)
```

## 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 `BridgeCapabilityHandler`s 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:

```kotlin theme={null}
DisposableEffect(controller) {
  onDispose {
    try {
      // Resolve any in-flight capture handlers first (rotation, navigation away, …)
      controller.cancelActiveCaptures("Journey screen disposed")
    } finally {
      controller.host.detach()
    }
  }
}
```

### 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.

```kotlin theme={null}
class JourneyViewModel : ViewModel() {
  val host = BridgeHost(hostVersion = "1.0.0")

  override fun onCleared() {
    try {
      host.dispose()
    } catch (e: Exception) {
      Log.w("JourneyViewModel", "Bridge teardown failed", e)
    }
  }
}
```

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(...)`.

```kotlin theme={null}
// Set up typed slots
host.documentCapture.handler = { request ->
  showCaptureUi()
  host.documentCapture.awaitCompletion()
}

// Register custom capabilities
host.registerCustomCapability("nfc.read") { request, responder ->
  // Handle the request, then call responder.respond(...)
}

// Then attach and load the journey
host.attach(webView)
webView.loadUrl(journeyUrl)
```

## 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.

```kotlin expandable theme={null}
import android.content.Intent
import android.webkit.WebResourceRequest
import android.webkit.WebView
import com.gbg.gbgbridge.webview.BridgeWebViewConfigurator

// The SDK's default bootstrap is internal — supply the literal yourself
// when installing a custom client.
private const val BOOTSTRAP_SCRIPT =
  "window.GBGBridge = window.GBGBridge || {}; " +
    "window.GBGBridge.receive = window.GBGBridge.receive || function(){};"

class JourneyWebViewClient(
  bootstrapScript: String,
) : BridgeWebViewConfigurator.BootstrapInjectingWebViewClient(bootstrapScript) {

  override fun shouldOverrideUrlLoading(
    view: WebView,
    request: WebResourceRequest,
  ): Boolean {
    // Custom navigation logic: open external links in the browser
    val url = request.url ?: return true
    if (url.host != "journey.example.com") {
      view.context.startActivity(Intent(Intent.ACTION_VIEW, url))
      return true
    }
    return false
  }
}

// Pass the client to attach() — not to a separate configure() call
host.attach(webView, client = JourneyWebViewClient(BOOTSTRAP_SCRIPT))
webView.loadUrl(journeyUrl)
```

<Note>
  **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.
</Note>

<Tip>
  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.
</Tip>

## Next Steps

* [Messaging Guide](/docs/go-v2/developer-integration/sdks/android/messaging) — Learn to send events and handle requests
* [Capability Handling Guide](/docs/go-v2/developer-integration/sdks/android/capability-handling) — Register handlers for native features
* [API Reference](/docs/go-v2/developer-integration/sdks/android/api-reference) — Detailed reference for `BridgeHost.attach` and `BridgeWebViewConfigurator`
