This page discusses the various methods and best practices for establishing a
native bridge, also known as JavaScript bridge, to facilitate communication
between web content in a WebView and a host Android application.
This enables web developers to use JavaScript to access native platform features—such as the camera, file system, or advanced hardware sensors—that standard web APIs don't normally provide.
Use cases
A JavaScript bridge implementation enables various integration scenarios where web content requires deeper access to the Android operating system. The following are some examples:
- Platform integration: Triggering native Android UI components (for
example, Biometric prompts,
BottomSheetDialog) from a web page. - Performance: Offloading heavy computational tasks to native Java or Kotlin code.
- Data persistence: Accessing local encrypted databases or shared preferences.
- Large data transfers: Passing media files or complex data structures between the app and the web renderer.
Communication mechanisms
Android offers three primary generations of APIs to establish a native bridge. While they are all still available, they differ significantly in security, usability, and performance.
Use addWebMessageListener (Recommended)
addWebMessageListener is the most modern and recommended approach for
communication between the web content and native app code. It combines the ease
of use of the JavaScript interface with the security of the messaging system.
How it works: The app adds a listener with a specific name and a set of
allowed origin rules. The WebView then ensures the JavaScript object is
present in the global scope (window.objectName) from the moment the page
begins to load.
Initialization: To ensure the WebView injects the JavaScript object before
any script runs, you must call addWebMessageListener before calling
loadUrl().
Key features:
Security and trust: Unlike legacy APIs, this method requires a
Set<String>ofallowedOriginRulesduring initialization. This is the primary mechanism for establishing trust.When you specify a trusted origin, such as
https://example.com, the WebView guarantees that it only exposes the injected JavaScript objects to web pages loaded from that exact origin.The native listener callback receives a
sourceOriginparameter with every message. You can use this to verify the exact origin of the sender if your bridge supports multiple allowed origins.Because the WebView strictly enforces these origin checks at the platform level, your app can generally rely upon messages received from a trusted
sourceOriginas truthful, eliminating the need for rigorous payload validation in most standard implementations.- WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
- WebView ignores paths. For example,
https://example.comallowshttps://example.com/loginandhttps://example.com/home. - WebView strictly limits wildcards to the start of the host for
subdomains. For example,
https://*.example.commatcheshttps://foo.example.combut nothttps://example.com. If you need to match bothhttps://example.comand its subdomains, you must add each origin rule separately to the allowlist (for example,"https://example.com", "https://*.example.com"). You can't use wildcards for the scheme or in the middle of a domain.
This restricts the bridge to verified domains, preventing unauthorized third-party content or injected iframes from executing native code.
Multi-frame support: Works across all frames that match the origin rules.
Threading: The listener callback runs on the application's main (UI) thread. If your bridge needs to handle complex data processing, JSON parsing, or database lookups, you must offload that work to a background thread to prevent freezing the application UI with an "app not responding" (ANR) error.
Bidirectional: When the web page sends a message, the app receives a
JavaScriptReplyProxythat it can use to send messages back to that specific frame. You can retain thisreplyProxyobject and use it at any time to send any number of messages to the page, not just to reply to each individual message the page sends. If the originating frame navigates away or is destroyed, messages sent usingpostMessage()on the proxy are silently ignored.App-side initiation: Although the web page must always initiate the communication channel with the app, the native app can unilaterally prompt the web page to begin this process. The native app can communicate to the web page with
addDocumentStartJavaScript()(to evaluate JavaScript before the page loads) orevaluateJavaScript()(to evaluate JavaScript after the page has loaded).
Limitation: This API sends data as either strings or byte[] arrays. For
more complicated data structures, such as, JSON objects, you must serialize this
to one of those formats and then deserialize on the other side to reconstruct
the data structure.
Usage example:
To understand the full sequence of a bidirectional message exchange, the events proceed in this order:
- Initiation (app): The native app registers the listener with
addWebMessageListenerand loads the web page withloadUrl(). - Message send (web): The web page's JavaScript calls
myObject.postMessage(message)to initiate the communication. - Message receive and reply (app): The app receives the message in the
listener callback and replies using the provided
replyProxy.postMessage(). - Reply receive (web): The web page receives the asynchronous reply in the
myObject.onmessage()callback function.
Kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
// Handle the message from JS
replyProxy.postMessage("Acknowledged!")
}
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val allowedOrigins = setOf("https://www.example.com")
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
Java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Handle the message from JS
replyProxy.postMessage("Acknowledged!");
};
// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
Set<String> allowedOrigins = Set.of("https://www.example.com");
WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
The following JavaScript demonstrates the client-side implementation of
addWebMessageListener, allowing the web content to receive messages from the
native app and send its own messages through the myObject proxy.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Use postWebMessage (Alternative)
Android introduced this to provide an asynchronous, messaging-based alternative
similar to the web's window.postMessage.
How it works: The app uses WebViewCompat.postWebMessage to send a payload
to the web page's main frame. To establish a bidirectional communication
channel, you can create a WebMessageChannel and pass one of its
ports with the message to the web content.
Characteristics:
- Asynchronous: Like
addWebMessageListener, this method uses asynchronous messaging, which ensures the web page remains responsive to user interactions while the app processes data in the background. - Origin aware: You can specify a
targetOriginto ensure the WebView delivers data only to a trusted website.
Limitations:
- Scope: This API limits communication to the main frame. It doesn't support directly addressing or sending messages to iframes.
- URI restrictions: You cannot use this method for content loaded using
data:URIs,file:URIs, orloadData(), unless you specify "*" as the target origin. Doing this lets any page receive the message. - Identity risk: There is no clear way for the web content to verify the sender's identity. A message that the web page receives could have originated from your native app or another iframe.
Use this method when you need a simple, async channel for string-based data in
earlier Android versions that don't support addWebMessageListener.
Use addJavascriptInterface (Legacy)
The oldest method involves injecting a native object instance directly into the WebView.
How it works: You define a Kotlin or Java class, annotate the allowed
methods with @JavascriptInterface, and add an instance of the class to the
WebView using addJavascriptInterface(Object, String).
Characteristics:
- Synchronous: The JavaScript execution environment blocks until the method in your Android code returns.
- Thread safety: The system calls methods on a background thread, requiring careful synchronization on the Kotlin or Java side.
- Security risk: By default,
addJavascriptInterfaceis available to every frame within the WebView, including iframes. It lacks origin-based access control. Due to the asynchronous behavior of WebView, it isn't possible to safely determine the URL of the frame that is calling your interface. You must not rely on methods likeWebView.getUrl()for security verification, as they aren't guaranteed to be accurate and don't indicate which specific frame made the request.
Summary of mechanisms
The following table provides a quick comparison of the three primary native bridge implementation mechanisms:
| Method | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementation | Asynchronous (Listener on main thread) | Asynchronous | Synchronous |
| Security | Highest (Allowlist-based) | High (Origin aware) | Low (No origin checks) |
| Complexity | Moderate | Moderate | Simple |
| Direction | Bidirectional | Bidirectional | Web to app |
| Minimum WebView version | Version 82 (and Jetpack Webkit 1.3.0) | Version 45 (and Jetpack Webkit 1.1.0) | All versions |
| Recommended | Yes | No | No |
Handle large data transfers
You must manage memory carefully when transferring large payloads, such as multi-megabyte strings or binary files, to avoid Application Not Responding (ANR) errors or crashes on 32-bit devices. This section discusses the various techniques and limitations associated with transferring significant amounts of data between the host application and web content.
Transfer binary data with byte arrays
With the WebMessageCompat class, you can send byte[] arrays directly
instead of serializing binary data into Base64 strings. Since Base64 adds
roughly 33% overhead to the data size, this is significantly more
memory-efficient and faster.
- Binary advantage: Transfer binary data like image files or audio between your native app and web content.
- Limitation: Even with byte arrays, the system copies data across the inter-process communication (IPC) boundary between the app and the isolated process that WebView uses to render the web content. This still consumes significant memory for very large files.
The following code examples demonstrate how to set up addWebMessageListener on
the native app side to receive messages marked with
WebMessageCompat.TYPE_ARRAY_BUFFER and optionally reply with binary data by
checking for WebViewFeature.MESSAGE_ARRAY_BUFFER.
Kotlin
fun setupWebView(webView: WebView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->
// Check if the received message is an ArrayBuffer
if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
val binaryData: ByteArray = message.arrayBuffer
// Process your binary data (image, audio, etc.)
println("Received bytes: ${binaryData.size}")
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
replyProxy.postMessage(replyBytes)
}
}
}
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
setOf("https://example.com"), // Security: restrict origins
listener
)
}
}
Java
public void setupWebView(WebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
// Check if the received message is an ArrayBuffer
if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
byte[] binaryData = message.getArrayBuffer();
// Process your binary data (image, audio, etc.)
System.out.println("Received bytes: " + binaryData.length);
// Optional: Send a binary reply back to JavaScript.
// This example sends a 3-byte array for simplicity.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
replyProxy.postMessage(replyBytes);
}
}
};
// "myBridge" matches the window.myBridge in JavaScript
WebViewCompat.addWebMessageListener(
webView,
"myBridge",
Set.of("https://example.com"), // Security: restrict origins
listener
);
}
}
The following JavaScript code demonstrates the client-side implementation of
addWebMessageListener, enabling the web content to send and receive binary
data (ArrayBuffer) to and from the native app using the window.myBridge
proxy injected in the previous example.
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
const response = await fetch('image.jpg');
const buffer = await response.arrayBuffer();
// Check if the injected bridge object exists
if (window.myBridge) {
// You can send the ArrayBuffer directly
window.myBridge.postMessage(buffer);
}
}
// Receiving binary data from the app
if (window.myBridge) {
window.myBridge.onmessage = function(event) {
if (event.data instanceof ArrayBuffer) {
console.log('Received binary data from App, length:', event.data.byteLength);
// Process the binary data (for example, as a Uint8Array)
const bytes = new Uint8Array(event.data);
console.log('First byte:', bytes[0]);
}
};
}
Efficient large-scale data loading
For very large files (>10 MB), use the shouldInterceptRequest method to
stream data:
- The web page initiates a
fetch()call to a custom, placeholder URL. For example,https://app.local/large-file. - The Android app intercepts this request in
WebViewClient.shouldInterceptRequest. - The app returns the data as an
InputStream.
This enables streaming data in chunks rather than loading the entire payload into memory at once.
The following JavaScript function demonstrates the client-side code for
efficiently loading a large binary file from the native application using a
standard fetch() call to a custom, placeholder URL.
async function fetchBinaryFromApp() {
try {
// This URL doesn't need to exist on the internet
const response = await fetch('https://app.local/data/large-file.bin');
if (!response.ok) throw new Error('Network response was not okay');
// For raw binary data:
const arrayBuffer = await response.arrayBuffer();
console.log('Received binary data, size:', arrayBuffer.byteLength);
// Process buffer (for example, new Uint8Array(arrayBuffer))
/*
// OR for an image:
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
document.getElementById('myImage').src = imageUrl;
*/
} catch (error) {
console.error('Fetch error:', error);
}
}
The following code examples demonstrate the native app side, using the
WebViewClient.shouldInterceptRequest method in both Kotlin and Java, to stream
a large binary file by intercepting a custom placeholder URL requested by the
web content.
Kotlin
webView.webViewClient = object : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest?
): WebResourceResponse? {
val url = request?.url ?: return null
// Check if this is our custom placeholder URL
if (url.host == "app.local" && url.path == "/data/large-file.bin") {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
val inputStream: InputStream = context.assets.open("my_data.pb")
// 2. Define Response Headers (Crucial for CORS/Fetch)
val headers = mutableMapOf<String, String>()
headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin
// 3. Return the response
return WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
)
} catch (e: Exception) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request)
}
}
Java
webView.setWebViewClient(new WebViewClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String urlPath = request.getUrl().getPath();
String host = request.getUrl().getHost();
// Check if this is our custom placeholder URL
if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
try {
// 1. Get your data as an InputStream
// (from Assets, Files, or a generated byte stream)
InputStream inputStream = getContext().getAssets().open("my_data.pb");
// 2. Define Response Headers (Crucial for CORS/Fetch)
Map<String, String> headers = new HashMap<>();
headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin
// 3. Return the response
return new WebResourceResponse(
"application/octet-stream", // MIME type (for example, image/jpeg)
"UTF-8", // Encoding
200, // Status Code
"OK", // Reason Phrase
headers, // Custom Headers
inputStream // The actual data stream
);
} catch (Exception e) {
// Handle exception
}
}
return super.shouldInterceptRequest(view, request);
}
});
Follow security recommendations
To protect your application and user data, follow these guidelines when implementing a bridge:
Enforce HTTPS: To ensure that malicious third-party content can't invoke your application's native logic, only allow communication with secure origins.
Rely on origin rules: The best way to deal with trust is to strictly define your
allowedOriginRulesand check thesourceOriginprovided in the message callback. Avoid using the full wildcard (*), which matches all origins, as your only origin rule unless absolutely necessary. Using wildcards for subdomains (for example,*.example.com) remains valid and secure for matching multiple subdomains (for example,foo.example.com,bar.example.com).Note: While origin rules protect against malicious third-party websites and hidden iframes, they can't protect against cross-site scripting (XSS) vulnerabilities within your own trusted domain. For example, if your web page displays user-generated content and is vulnerable to stored XSS, an attacker could execute a script acting as your trusted origin. Consider applying validation to the message payloads before executing sensitive native platform operations.
Minimize surface area: Only expose the specific methods or data that the web page requires.
Check features at runtime: Recent bridge APIs, including
addWebMessageListener, are part of the Jetpack Webkit library. So, always check for support usingWebViewFeature.isFeatureSupported()before calling them.