Access native APIs with JavaScript bridge

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> of allowedOriginRules during 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 sourceOrigin parameter 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 sourceOrigin as 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.com allows https://example.com/login and https://example.com/home.
    • WebView strictly limits wildcards to the start of the host for subdomains. For example, https://*.example.com matches https://foo.example.com but not https://example.com. If you need to match both https://example.com and 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 JavaScriptReplyProxy that it can use to send messages back to that specific frame. You can retain this replyProxy object 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 using postMessage() 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) or evaluateJavaScript() (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:

  1. Initiation (app): The native app registers the listener with addWebMessageListener and loads the web page with loadUrl().
  2. Message send (web): The web page's JavaScript calls myObject.postMessage(message) to initiate the communication.
  3. Message receive and reply (app): The app receives the message in the listener callback and replies using the provided replyProxy.postMessage().
  4. 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 targetOrigin to 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, or loadData(), 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, addJavascriptInterface is 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 like WebView.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:

  1. The web page initiates a fetch() call to a custom, placeholder URL. For example, https://app.local/large-file.
  2. The Android app intercepts this request in WebViewClient.shouldInterceptRequest.
  3. 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 allowedOriginRules and check the sourceOrigin provided 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 using WebViewFeature.isFeatureSupported() before calling them.