Skip to main content
A step-by-step walkthrough from zero to a working GBGBridge integration. Follow this page in order; each step links to the detailed guide if you need more context.

Prerequisites

  • An Android project with minSdk 24 (Android 7.0) or higher and compileSdk 34
  • JDK 17 and Kotlin 2.x
  • Access to Maven Central (no credentials or extra repositories required)
  • A journey URL from the GBG Go Core SDK (see Journey URL)

Step 1: Add the SDK

Add GBGBridge from Maven Central in your module’s build.gradle.kts:
dependencies {
  implementation("com.gbg:gbgbridge-sdk:0.1.0-alpha01")
}
Make sure mavenCentral() is in your repository list β€” no other repository setup is needed.

Step 2: Obtain a journey URL

Your app needs a journey URL to load inside the bridge WebView. This URL is generated server-side using the GBG Go Core SDK β€” your backend calls the Core SDK, receives a session URL, and passes it to the Android app.
See Journey URL for the full pattern, including URL shape, authentication, and configuration options.

Step 3: Add manifest entries

Declare the permissions your integration uses in AndroidManifest.xml:
<!-- Required: the WebView loads the journey over the network -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- Required for document and selfie capture -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
    android:name="android.hardware.camera"
    android:required="false" />
Unlike iOS usage descriptions, the CAMERA permission must also be requested at runtime β€” handle that inside your capture surface (see Capture Screens). Android blocks cleartext HTTP by default (API 28+) β€” the equivalent of App Transport Security on iOS. If you load a journey from a local development server, add a network_security_config.xml that permits cleartext only for 10.0.2.2, localhost, and 127.0.0.1, ideally in a debug source set.
Never ship android:usesCleartextTraffic="true" unscoped in production. Production journey URLs must be HTTPS. See the Security Guide.

Step 4: Initialize BridgeHost

Create a BridgeHost β€” this is the coordinator that routes messages between the WebView and your native code. In Compose, hold it in remember { } (or a ViewModel) so it survives recomposition; in the View system, keep it as a property on your Activity or Fragment.
import com.gbg.gbgbridge.core.BridgeHost

val host = remember { BridgeHost(hostVersion = "1.0.0") }
BridgeHost is main-thread-only: state-mutating methods throw IllegalStateException when called off the main thread. From a coroutine, hop back with withContext(Dispatchers.Main) before touching the host.
host.delegate is held via a WeakReference β€” the host does not keep your delegate alive. Store the delegate in a property with its own strong reference; an inline host.delegate = MyDelegate() will silently stop firing once the delegate is garbage-collected.

Step 5: Set up capture handlers

Attach handlers to the typed capability slots. Setting a handler declares that your app supports that capability β€” no separate configuration step.

Option A: Your Own Capture Surface (Development / Early Integration)

Android ships no built-in stub views β€” your app supplies the capture UI. The handler pattern is to suspend until your UI completes the capture:
host.documentCapture.handler = { request ->
  host.documentCapture.awaitCompletion()
}

host.selfieCapture.handler = { request ->
  host.selfieCapture.awaitCompletion()
}
Then present your capture surface when a request arrives by collecting the activeRequest state flow, and resolve the request from the UI:
val documentRequest by host.documentCapture.activeRequest.collectAsState()

if (documentRequest != null) {
  DocumentCaptureScreen(
    onCaptured = { imageData, width, height ->
      host.documentCapture.complete(
        CaptureResult.Document(imageData, width, height)
      )
    },
    onCancelled = {
      host.documentCapture.cancelIfBusy("User dismissed")
    }
  )
}
See Capture Screens for capture-UI patterns: a CameraX live preview, the system photo picker, and a placeholder bitmap for emulator development.

Option B: Smart Capture SDKs (Production)

Smart Capture integration for Android is not yet available. The capture surface is designed to be swappable β€” the handler setup above stays identical, and only the UI presented when a request arrives changes.

Step 6: Set permission state

Report camera permission state so the web journey can check permissions before attempting capture:
val camera = CameraDetector.check(context)
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
CameraDetector only distinguishes GRANTED from NOT_DETERMINED β€” Android cannot tell never-asked apart from permanently denied without app-side state. After running your own runtime-permission flow, set the richer DENIED state on the slots yourself.

Step 7: Display the journey

There is no BridgeWebView wrapper on Android. Create a plain WebView, attach the host, and load the journey URL β€” attach() configures the WebView for you (enables JavaScript and DOM storage, installs the bootstrap-injecting WebViewClient and a default WebChromeClient, and registers the JavaScript interface).
@Composable
fun JourneyView(host: BridgeHost, journeyUrl: String) {
  AndroidView(
    factory = { ctx ->
      WebView(ctx).also { webView ->
        host.attach(webView)
        webView.loadUrl(journeyUrl)
      }
    },
    modifier = Modifier.fillMaxSize()
  )

  DisposableEffect(Unit) {
    onDispose {
      host.documentCapture.cancelIfBusy("Screen closed")
      host.selfieCapture.cancelIfBusy("Screen closed")
      host.detach()
    }
  }
}
A few rules keep the bridge wiring intact:
  • Custom WebViewClient β€” if you need your own navigation policy or error logging, subclass BootstrapInjectingWebViewClient and pass it via host.attach(webView, client = ...). Setting a plain WebViewClient yourself would remove bootstrap injection; call super.onPageStarted(...) in your subclass to keep it.
  • Custom WebChromeClient β€” set it after attach(). Attaching installs a default chrome client and overwrites whatever was there before.
  • SSL errors β€” never call handler.proceed() in onReceivedSslError. Cancel the load and surface the failure instead.
  • Lifecycle β€” call detach() when the screen goes away but the host may be reused, and dispose() for terminal teardown (Compose DisposableEffect.onDispose, Activity onDestroy, or ViewModel onCleared). After dispose() the host cannot be reused. detach() cancels in-flight typed-slot captures automatically, but cancel them explicitly first when you want to control the cancellation reason β€” and you must cancel manually if you registered raw handlers instead of the typed slots.
See Embedding Guide for Compose and View-system integration patterns.

Step 8: Build and Run

  1. Build and run on a physical device for real camera capture; on the emulator, use the photo-picker or placeholder-bitmap paths from Capture Screens.
  2. The web journey loads in the WebView.
  3. When the journey reaches a document or selfie step, the bridge sends a capture request.
  4. Your handler runs, your capture surface presents, the user captures, and the result flows back to the web journey.
If your journey URL comes from a local development server, remember that localhost on the emulator is the emulator itself β€” use 10.0.2.2 to reach your host machine, or run adb reverse tcp:3000 tcp:3000.

Verify It Works

Check for these signs of a successful integration:
  • The web journey loads and renders correctly in the WebView.
  • capability.query is handled automatically β€” the journey knows which capabilities your app supports.
  • Document capture requests trigger your capture surface, and the captured image is returned to the journey.
  • Selfie capture requests trigger your selfie surface, and the result is returned.
  • Cancellation flows work β€” dismissing the capture surface sends a cancelled response.

Common Issues

SymptomLikely CauseFix
Blank WebViewCleartext HTTP blocked (API 28+)Use HTTPS, or add a network_security_config.xml scoped to 10.0.2.2/localhost for local dev
Blank WebView on emulator with a localhost URLlocalhost is the emulator itselfUse 10.0.2.2, or adb reverse tcp:3000 tcp:3000
capability.query returns emptyNo handlers setSet handler on at least one typed slot before loading the URL
Capture surface never appearsactiveRequest not collectedCollect host.documentCapture.activeRequest (collectAsState()) and show your surface when it is non-null
Capture result not received by journeycomplete() not calledEnsure every exit path calls complete(...) or cancelIfBusy(...)
Delegate callbacks stop firingDelegate was garbage-collected (WeakReference)Hold a strong reference to your delegate
IllegalStateException from host methodsCalled off the main thread, or after dispose()Call from the main thread (withContext(Dispatchers.Main)); never reuse a disposed host
See Troubleshooting for a comprehensive diagnostic guide.

Complete Minimal Example

Putting it all together β€” a minimal Compose screen that loads a journey and handles document capture with your own capture surface:
import android.webkit.WebView
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.gbg.gbgbridge.capabilities.CameraDetector
import com.gbg.gbgbridge.capabilities.CaptureResult
import com.gbg.gbgbridge.core.BridgeHost

@Composable
fun JourneyScreen(journeyUrl: String) {
  val context = LocalContext.current
  val host = remember {
    BridgeHost(hostVersion = "1.0.0").apply {
      documentCapture.permissionState =
        CameraDetector.check(context).permissionState
      documentCapture.handler = { request ->
        documentCapture.awaitCompletion()
      }
    }
  }
  val documentRequest by host.documentCapture.activeRequest.collectAsState()

  AndroidView(
    factory = { ctx ->
      WebView(ctx).also { webView ->
        host.attach(webView)
        webView.loadUrl(journeyUrl)
      }
    },
    modifier = Modifier.fillMaxSize()
  )

  if (documentRequest != null) {
    // Your capture surface β€” CameraX preview, photo picker, or placeholder
    DocumentCaptureScreen(
      onCaptured = { imageData, width, height ->
        host.documentCapture.complete(
          CaptureResult.Document(imageData, width, height)
        )
      },
      onCancelled = {
        host.documentCapture.cancelIfBusy("User dismissed")
      }
    )
  }

  DisposableEffect(Unit) {
    onDispose {
      host.documentCapture.cancelIfBusy("Screen closed")
      try {
        host.dispose()
      } catch (e: Exception) {
        // The WebView may already be shutting down β€” log and move on
      }
    }
  }
}
DocumentCaptureScreen is your own composable β€” see Capture Screens for ready-made patterns.
See Hello Journey for the full annotated example.

What’s Next

Once the basic integration is working:
  • Add selfie capture β€” same pattern as document capture, using host.selfieCapture.
  • Add custom capabilities β€” see Capability Handling.
  • Build a production-quality capture surface with CameraX β€” see Capture Screens.
  • Review the Security Guide before shipping to production.