Android WebView
On Android, nBridge talks to a JavascriptInterface object injected into the WebView.
The contract:
- Web → native: nBridge calls
window.AndroidBridge.postMessage(jsonString)— one JSON string per message. The interface name is configurable viaandroidInterface(default"AndroidBridge"). - Native → web: your app evaluates
window.sendBridgeMessage(jsonString)— nBridge attaches this global at initialization.
The full message shape, response convention, handshake, batching, and compression rules are specified in the Wire Protocol.
Web side
import { createBridge } from "nbridge";
const bridge = createBridge({
androidInterface: "AndroidBridge", // must match addJavascriptInterface(...)
handshake: { enabled: true }, // recommended once the native side acks
});Detection is automatic: when window.AndroidBridge exists, the Android adapter is used.
Native side (Kotlin)
1. The bridge object
import android.webkit.JavascriptInterface
import android.webkit.WebView
import org.json.JSONObject
class NBridge(private val webView: WebView) {
@JavascriptInterface
fun postMessage(message: String) {
try {
val json = JSONObject(message)
val type = json.getString("type")
val payload = json.optJSONObject("payload")
val id = json.optString("id")
when (type) {
// Handshake: reply so the web side's waitForReady() resolves
"__nbridge_handshake__" -> {
sendToWeb(JSONObject().put("type", "__nbridge_handshake_ack__"))
}
// Batch envelope: unpack and process each entry
"__nbridge_batch__" -> {
val messages = payload?.optJSONArray("messages") ?: return
for (i in 0 until messages.length()) {
postMessage(messages.getJSONObject(i).toString())
}
}
// A request from the web — answer with "<type>_response" + same id
"getUser" -> {
val userId = payload?.optString("id")
val user = JSONObject()
.put("name", "Ada Lovelace")
.put("email", "ada@example.com")
if (id.isNotEmpty()) {
sendToWeb(
JSONObject()
.put("type", "getUser_response")
.put("id", id)
.put("payload", user)
)
}
}
// Fire-and-forget events
"shutdown" -> { /* finish() the hosting Activity */ }
else -> android.util.Log.w("NBridge", "Unhandled type: $type")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun sendToWeb(message: JSONObject) {
// JSON-escape the payload as a JS string literal
val js = "window.sendBridgeMessage(${JSONObject.quote(message.toString())})"
webView.post {
webView.evaluateJavascript(js, null)
}
}
// Push an event to the web at any time
fun emit(type: String, payload: Any?) {
sendToWeb(JSONObject().put("type", type).put("payload", payload))
}
}2. Register it
val webView = findViewById<WebView>(R.id.webview)
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(NBridge(webView), "AndroidBridge") // name must match config
webView.loadUrl("https://your-app.example.com")3. Reporting a failure
To reject a web request instead of answering it, reply with <type>_error and an { error } payload:
sendToWeb(
JSONObject()
.put("type", "getUser_error")
.put("id", id)
.put("payload", JSONObject().put("error", "User not found"))
)Escaping
Always build the evaluateJavascript call with proper escaping (JSONObject.quote above). Interpolating raw JSON into single quotes breaks on payloads containing quotes or newlines.
Passing the host version
Host Rules vary UI and behavior by app version. The simplest way to tell the web side which version it is running in is to append ?hv=<version> to the URL you load — the zero-config versionFromQuery("hv") source reads it:
val appVersion = packageManager.getPackageInfo(packageName, 0).versionName
webView.loadUrl("https://your-app.example.com/?hv=$appVersion")The version is persisted to sessionStorage, so it survives client-side navigation that drops the param. If you'd rather deliver it over the bridge (e.g. via emit("hostInfo", …)), call host.setVersion(version) when it arrives instead — see async acquisition.
Troubleshooting
- Web sends, native never receives — check
javaScriptEnabled = trueand that theaddJavascriptInterfacename exactly matchesandroidInterface. EnableWebView.setWebContentsDebuggingEnabled(true)and inspect viachrome://inspect. - Native sends, web never receives — use
evaluateJavascript(notloadUrl("javascript:…")), wrap inwebView.post { }for thread safety, and verify the JSON string escaping. waitForReady()times out — the native side is not answering__nbridge_handshake__. Add the handshake case above, or disablehandshake.enabledon the web side.- Batches arrive as one weird message — implement the
__nbridge_batch__unpack case, or disable batching.