Reference app: The complete source code for this tutorial is available at gbg-go-android-reference. You can clone it and run it immediately, or follow this tutorial to build it step by step.
Prerequisites
You also need the GBGBridge Android SDK, published to Maven Central as
com.gbg:gbgbridge-sdk. See the Getting Started guide for installation details.
Set Up the Backend
The Android app does not call the GBG Go API directly. Instead, it calls a lightweight companion server that creates journey sessions using the GBG Go Core SDK. This keeps your API credentials on the server, never on the device.1. Clone the reference repository
2. Install dependencies
3. Configure credentials
Copy the example environment file and fill in your GBG Go credentials:.env in a text editor:
4. Start the server
http://localhost:3000.
5. Test the server
Open a new terminal and verify it responds:{"ok":true}
Leave the server running. You will need it for the rest of the tutorial.
Create Your Android Project
Open Android Studio and create a new project:- File → New → New Project
- Choose the Empty Activity template (the Compose/Material 3 one).
- Set the name to GBG Go Reference and the package to
com.gbg.go.reference. - Set the minimum SDK to API 24.
- Click Finish.
Add GBGBridge via Gradle
The SDK is on Maven Central, so no repository setup is needed beyond themavenCentral() entry that new projects already have. In app/build.gradle.kts, add the serialization plugin and the dependencies the app uses:
app/build.gradle.kts for the complete file.
Configure the manifest
Add these entries toAndroidManifest.xml:
CAMERA is requested at runtime later; declaring the camera feature as required="false" keeps the app installable on devices without one (the capture surface falls back to the photo picker).
The networkSecurityConfig attribute is the Android equivalent of iOS’s NSAllowsLocalNetworking: Android blocks cleartext HTTP by default (API 28+), so requests to the local companion server would fail without it. Create res/xml/network_security_config.xml scoped to loopback hosts only:
File structure
Create the following package structure underapp/src/main/java/com/gbg/go/reference/:
App Entry Point and Navigation
The app is a singleComponentActivity hosting a Compose NavHost with three destinations: Setup, Journey, and Result. Replace the contents of MainActivity.kt with:
Configuration Screen
CreateSetupScreen.kt in the ui/setup package:
- Server URL — the companion server address. Defaults to
http://10.0.2.2:3000, which works on the Android emulator (10.0.2.2is the emulator’s loopback alias for the host machine). On a physical device, use your machine’s LAN IP address (e.g.http://192.168.1.100:3000). - Resource ID — identifies which journey template to run. Leave blank to use the server’s default.
SharedPreferences.
Calling Your Backend
CreateJourneyService.kt in the ui/journey package:
JourneyService makes a single POST request to the companion server’s /api/journey/start endpoint. The server authenticates with GBG Go, creates a journey session, registers a mobile device, and returns a journey URL. The Android app never touches API credentials directly.
The StartResponse includes:
The connect token expires quickly. If the journey URL fails to load, go back and tap “Start Journey” again to get a fresh token.
JourneyService also has a fetchState(serverUrl, instanceId) function that calls the companion server’s GET /api/journey/:instanceId/state endpoint. The Result screen uses it at the end of the tutorial to fetch the authoritative journey outcome.
The Bridge Integration
This is the core of the integration. CreateJourneyScreen.kt in the ui/journey package. It contains three pieces: a BridgeController that owns the host, a delegate that observes bridge traffic, and the composable that wires both to a WebView.
Start with the controller and delegate:
WebView and presents the capture surface when a request arrives:
WebViewClient and WebChromeClient for diagnostics. The WebViewClient subclasses the SDK’s BridgeWebViewConfigurator.BootstrapInjectingWebViewClient so the bridge bootstrap keeps working while adding logging for failures that otherwise present as a silent blank page:
How the Bridge Works
TheBridgeHost is the native side of a bidirectional communication channel between your Kotlin code and the JavaScript running inside the WebView. When the web journey needs a native capability — like capturing a document photo — it sends a request message through the bridge by calling window.GBGBridge.postMessage(...). Your Kotlin code handles the request and sends a response back, which the web receives via window.GBGBridge.receive(...).
Unlike iOS, there is no BridgeWebView wrapper on Android. You create a plain WebView (here via Compose’s AndroidView) and call host.attach(webView). The attach call configures the WebView for you: it enables JavaScript and DOM storage, registers the GBGBridge JavaScript interface, and installs a WebViewClient that injects the bootstrap script when each page starts loading. You provide the WebView and the URL — everything else is automatic.
The Capability Handler Pattern
The journey requests captures using the wire-level actionscamera.document.capture and camera.selfie.capture. The app handles them by registering a BridgeCapabilityHandler for each action — a small interface with an action property and a handle(request, responder) method:
handle is synchronous — it does not suspend or block. For async work like presenting a camera UI, the pattern is: store the responder, return immediately, and call responder.respond(...) later on the main thread when the user finishes. The responder accepts exactly one response; subsequent calls are no-ops.
The full request lifecycle:
- The web journey sends a
camera.document.capturerequest through the bridge. - The host dispatches it to the registered handler on the main thread.
- The handler stores the responder and fires
onActivate, which setsactiveCaptureCompose state. - Compose presents
CaptureStubScreenas a full-screen dialog. - The user captures a photo, picks one from the library, or cancels.
- The UI callback routes the result to the handler, which calls
responder.respond(...)withSUCCESS,CANCELLED, orERROR. - The handler clears its responder and fires
onDeactivate, dismissing the capture surface. - The bridge delivers the response to the journey under the request’s correlation ID.
The SDK also provides built-in typed slots —
host.documentCapture and host.selfieCapture, each a CaptureCapability with a suspending handler and an activeRequest StateFlow. They are the closest Android analogue to the pattern the iOS tutorial uses. The reference app deliberately uses raw handlers to show the general mechanism that works for any capability. See Capability Handling for both approaches.Why the delegate is strongly held
BridgeHost.delegate is stored as a WeakReference — the host does not keep your delegate alive. An inline assignment like host.delegate = LoggingDelegate() with no other strong reference works until the next garbage collection, then callbacks silently stop firing. This is the Android mirror image of iOS’s [weak host] capture advice: on iOS you weaken your reference to avoid a retain cycle; on Android the SDK already holds weakly, so you must hold strongly. That is why BridgeController — itself pinned by remember { } — owns the delegate as a property.
Why the client goes to attach()
host.attach(webView, client = ...) runs the SDK’s WebView configuration internally, installing the client you pass (or a default one) plus a plain WebChromeClient. Two ordering rules follow:
- Do not call
BridgeWebViewConfigurator.configure()yourself beforeattach()— attach would re-run configuration and clobber it. - Set your custom
WebChromeClientafterattach(), or the configurator’s plain one overwrites yours.
BootstrapInjectingWebViewClient and calls super.onPageStarted, the bridge bootstrap script keeps being injected on every page load. The bootstrap literal is passed to the subclass constructor since the SDK’s default is internal.
Why DisposableEffect cancels then detaches
When the user navigates away (or the journey ends), onDispose runs two steps in order. First it cancels any in-flight capture handler so the JS side receives a CANCELLED response instead of hanging forever on a reply that will never arrive. Then it calls host.detach(), which removes the JavaScript interface and tears down the WebView↔host edge. Detach is idempotent, so it is safe even if the screen is disposed twice.
Observing terminal events
When the journey reaches a terminal state, the web side emits a one-way bridge EVENT —journey.completed, journey.failed, or journey.abandoned — carrying instanceId and interactionId in its data. Events do not go through the request/response handler dispatch path, so the place to observe them is the delegate’s onMessage. The LoggingDelegate above matches on the action, extracts the IDs, and invokes onTerminalEvent, which navigates to the Result screen.
The bridge event tells you that the journey ended; the GBG Go journeys.getState API tells you what the outcome was. The reference app’s ResultScreen demonstrates the second step with a “Fetch outcome” button that calls the companion server’s GET /api/journey/:instanceId/state endpoint. In production, webhook-driven backends are usually preferable to polling from the device.
Handling Capture Requests
TheCaptureStubScreen is part of the reference app (the Android SDK does not ship built-in capture views — see Capture Screens). It is a unified, full-screen Compose Dialog used for both document and selfie requests, offering three rungs of capture affordance in priority order:
- Live CameraX preview + shutter button — back camera for documents, front camera for selfies, with a framing guide overlay. This is the path a production app would replace with a dedicated capture SDK.
- System photo picker (
ActivityResultContracts.PickVisualMedia) — for choosing an existing image. Useful on devices without a usable camera. Backing out of the picker does not cancel the capture request; the user returns to the capture surface. - Synthetic placeholder bitmap — a tertiary “Use placeholder image” button so the emulator path can exercise the end-to-end bridge round-trip without a real camera.
CAMERA permission is requested at runtime inside the screen. If it is denied, or the device has no camera, the preview area falls back to a static framing guide and the library and placeholder options carry the user through.
The screen accepts callbacks rather than touching the bridge directly:
Each callback routes to the matching
CameraCaptureHandler method, which sends the response — a base64-encoded JPEG with its dimensions and MIME type — back to the journey.
Run the App
- Make sure the companion server is running. Run
node index.mjsin theserver/directory. - Start an Android emulator (API 24+) or connect a device.
- Press Run in Android Studio (or build with
./gradlew assembleDebug). - On the Setup screen, leave the server URL as
http://10.0.2.2:3000(emulator). - Tap Start Journey.
- The journey loads in the WebView. When it requests a document capture, the capture surface appears as a full-screen overlay.
- Tap the shutter to capture — or Use placeholder image on an emulator without a camera feed. The result is sent back to the journey.
- When the journey finishes, the app navigates to the Result screen, where Fetch outcome retrieves the authoritative state from the companion server.
Common Pitfalls
A few rough edges trip up most teams running the tutorial app for the first time — emulator-vs-device URLs, Android’s cleartext-HTTP block, and the SDK’s weak delegate reference. The notes below explain how to avoid them.Emulator vs device
From inside the emulator,localhost and 127.0.0.1 route to the emulator itself, not your machine. Use the loopback alias http://10.0.2.2:3000 (the Setup screen’s default), or forward the port and keep using 127.0.0.1:
localhost at all — use your machine’s LAN IP instead (e.g. http://192.168.1.100:3000), and make sure your network security config permits cleartext to that host (the reference app does this with a permissive debug-source-set override).
Cleartext HTTP is blocked by default
Android blocks cleartext HTTP on API 28+. Without anetwork_security_config.xml allowlisting your dev hosts, requests to the companion server fail — and a blocked journey URL presents as a blank WebView. If the Setup screen shows a network error or the journey never loads, check the network security config first. The diagnostic WebViewClient and the onConsoleMessage forwarding in the tutorial exist precisely to make these failures visible in logcat; on debuggable builds you can also inspect the WebView via chrome://inspect.
Camera on the emulator
The emulator’s camera feed is synthetic or absent. The capture surface degrades gracefully: use the system photo picker or the Use placeholder image button to complete the round-trip. This is expected behaviour, not a bug.Connect token expiry
The connect token from the server is short-lived (~120 seconds). If the journey URL does not load in time, go back and tap “Start Journey” again for a fresh token.The delegate is weakly referenced
host.delegate is a WeakReference. Assigning an inline instance with no other strong reference (host.delegate = LoggingDelegate()) works briefly, then callbacks silently stop after garbage collection. Keep the delegate as a property of an object that outlives the host — like the remember-pinned BridgeController in this tutorial.
WebView client ordering
Pass your customWebViewClient to attach(webView, client = ...) rather than calling configure() separately first, and set any custom WebChromeClient after attach(). Getting the order wrong silently replaces your clients, which usually surfaces as missing console logs or a bootstrap that never injects.
Cancel handlers before detach
If the screen is disposed while a capture request is in flight, detaching without responding leaves the web journey waiting forever. Always cancel active handlers (sending aCANCELLED response) before calling host.detach(), as the DisposableEffect in this tutorial does.
What’s Next
- Tutorial Part 2: Integrate Smart Capture SDKs — Replace the stub capture surface with production document scanning and face capture with liveness detection.
- API Reference — Full documentation for
BridgeHost,BridgeWebViewConfigurator,CaptureCapability, and all message types. - Concepts — Deeper dive into the bridge architecture, message protocol, and capability system.
- Capability Handling — Raw handlers, typed slots, and custom capabilities in depth.
- Capture Screens — Capture-UI patterns: CameraX preview, the system photo picker, and placeholder paths.
- View-system integration — Use
BridgeHost.attach(...)with aWebViewin an Activity or Fragment instead of Compose. See Embedding.