JavaScript ブリッジを使用してネイティブ API にアクセスする

このページでは、WebView 内のウェブ コンテンツとホスト Android アプリケーション間の通信を容易にするために、ネイティブ ブリッジ(JavaScript ブリッジとも呼ばれます)を確立するためのさまざまな方法とベスト プラクティスについて説明します。

これにより、ウェブ デベロッパーは JavaScript を使用して、標準のウェブ API では通常提供されないカメラ、ファイル システム、高度なハードウェア センサーなどのネイティブ プラットフォーム機能にアクセスできます。

ユースケース

JavaScript ブリッジの実装により、ウェブ コンテンツが Android オペレーティング システムへのより深いアクセスを必要とするさまざまな統合シナリオが可能になります。以下に、いくつかの例を示します。

  • プラットフォーム統合: ウェブページからネイティブ Android UI コンポーネント(生体認証プロンプト、BottomSheetDialog など)をトリガーします。
  • パフォーマンス: 重い計算タスクをネイティブ Java または Kotlin コードにオフロードします。
  • データの永続性: ローカルの暗号化されたデータベースまたは共有設定にアクセスします。
  • 大規模なデータ転送: アプリとウェブ レンダラの間でメディア ファイルや複雑なデータ構造を渡す。

通信メカニズム

Android では、ネイティブ ブリッジを確立するための 3 つの主要な世代の API が提供されています。いずれも引き続き利用できますが、セキュリティ、ユーザビリティ、パフォーマンスは大きく異なります。

addWebMessageListener を使用する(推奨)

addWebMessageListener は、ウェブ コンテンツとネイティブ アプリコード間の通信に最も適した最新のアプローチです。JavaScript インターフェースの使いやすさとメッセージング システムのセキュリティを組み合わせたものです。

仕組み: アプリは、特定の名前と一連の許可されたオリジン ルールを持つリスナーを追加します。WebView は、ページの読み込みが開始された時点から、JavaScript オブジェクトがグローバル スコープ(window.objectName)に存在するようにします。

初期化: WebView がスクリプトを実行する前に JavaScript オブジェクトを挿入するようにするには、loadUrl() を呼び出す前に addWebMessageListener を呼び出す必要があります。

主な機能:

  • セキュリティと信頼性: 以前の API とは異なり、このメソッドでは初期化時に allowedOriginRulesSet<String> が必要です。これは、信頼を確立するための主要なメカニズムです。

    https://example.com などの信頼できるオリジンを指定すると、WebView は、挿入された JavaScript オブジェクトを、そのオリジンから読み込まれたウェブページにのみ公開することを保証します。

    ネイティブ リスナー コールバックは、すべてのメッセージで sourceOrigin パラメータを受け取ります。ブリッジが複数の許可されたオリジンをサポートしている場合、これを使用して送信者の正確なオリジンを確認できます。

    WebView はプラットフォーム レベルでこれらのオリジン チェックを厳密に実施するため、アプリは一般的に、信頼できる sourceOrigin から受信したメッセージを真実であると見なすことができます。これにより、ほとんどの標準的な実装で厳格なペイロード検証を行う必要がなくなります。

    • WebView は、スキーム(HTTP/HTTPS)、ホスト、ポートに対してルールを照合します。
    • WebView はパスを無視します。たとえば、https://example.comhttps://example.com/loginhttps://example.com/home を許可します。
    • WebView では、サブドメインのホストの先頭にワイルドカードを厳密に制限しています。たとえば、https://*.example.comhttps://foo.example.com と一致しますが、https://example.com とは一致しません。https://example.com とそのサブドメインの両方を照合する必要がある場合は、各オリジンルールを個別に許可リストに追加する必要があります(例: "https://example.com", "https://*.example.com")。スキームやドメインの中間でワイルドカードを使用することはできません。

    これにより、ブリッジが検証済みのドメインに制限され、未承認のサードパーティ コンテンツや挿入された iframe がネイティブ コードを実行できなくなります。

  • マルチフレーム サポート: オリジン ルールに一致するすべてのフレームで動作します。

  • スレッド処理: リスナー コールバックは、アプリのメイン(UI)スレッドで実行されます。ブリッジで複雑なデータ処理、JSON 解析、データベース ルックアップを処理する必要がある場合は、その処理をバックグラウンド スレッドにオフロードして、アプリケーション UI が ANR(アプリケーション応答なし)エラーでフリーズしないようにする必要があります。

  • 双方向: ウェブページがメッセージを送信すると、アプリは JavaScriptReplyProxy を受け取ります。この JavaScriptReplyProxy を使用して、特定のフレームにメッセージを送信し返すことができます。この replyProxy オブジェクトは保持でき、ページから送信された個々のメッセージへの返信だけでなく、いつでもページに任意の数のメッセージを送信するために使用できます。元のフレームが移動または破棄された場合、プロキシの postMessage() を使用して送信されたメッセージは無視されます。

  • アプリ側の開始: ウェブページは常にアプリとの通信チャネルを開始する必要がありますが、ネイティブ アプリは一方的にウェブページにこのプロセスの開始を促すことができます。ネイティブ アプリは、addDocumentStartJavaScript()(ページが読み込まれる前に JavaScript を評価する)または evaluateJavaScript()(ページが読み込まれた後に JavaScript を評価する)を使用して、ウェブページと通信できます。

制限事項: この API は、文字列または byte[] 配列としてデータを送信します。JSON オブジェクトなどの複雑なデータ構造の場合は、これらの形式のいずれかにシリアル化してから、反対側で逆シリアル化してデータ構造を再構築する必要があります。

使用例:

双方向メッセージ交換の完全なシーケンスを把握するには、イベントが次の順序で進行します。

  1. 開始(アプリ): ネイティブ アプリが addWebMessageListener でリスナーを登録し、loadUrl() でウェブページを読み込みます。
  2. メッセージ送信(ウェブ): ウェブページの JavaScript が myObject.postMessage(message) を呼び出して通信を開始します。
  3. メッセージの受信と返信(アプリ): アプリはリスナー コールバックでメッセージを受信し、提供された replyProxy.postMessage() を使用して返信します。
  4. 返信の受信(ウェブ): ウェブページは myObject.onmessage() コールバック関数で非同期の返信を受信します。

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);
}

次の JavaScript は、addWebMessageListener のクライアントサイドの実装を示しています。これにより、ウェブ コンテンツはネイティブ アプリからメッセージを受信し、myObject プロキシを介して独自のメッセージを送信できます。

myObject.onmessage = function(event) {
    console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");

postWebMessage(代替)を使用する

Android は、ウェブの window.postMessage と同様の非同期メッセージ ベースの代替手段を提供するために、これを導入しました。

仕組み: アプリは WebViewCompat.postWebMessage を使用して、ウェブページのメインフレームにペイロードを送信します。双方向通信チャネルを確立するには、WebMessageChannel を作成し、そのポートの 1 つをメッセージとともにウェブ コンテンツに渡します。

特徴:

  • 非同期: addWebMessageListener と同様に、このメソッドは非同期メッセージングを使用します。これにより、アプリがバックグラウンドでデータを処理している間も、ウェブページはユーザー操作に応答し続けます。
  • オリジン認識: targetOrigin を指定して、WebView が信頼できるウェブサイトにのみデータを配信するようにできます。

制限:

  • スコープ: この API は、通信をメインフレームに制限します。iframe への直接のアドレス指定やメッセージの送信はサポートされていません。
  • URI の制限: ターゲットのオリジンとして「*」を指定しない限り、data: URI、file: URI、または loadData() を使用して読み込まれたコンテンツに対してこのメソッドを使用することはできません。これにより、どのページでもメッセージを受信できるようになります。
  • ID のリスク: ウェブ コンテンツで送信者の ID を確認する明確な方法がない。ウェブページが受け取るメッセージは、ネイティブ アプリまたは別の iframe から送信された可能性があります。

addWebMessageListener をサポートしていない以前の Android バージョンで、文字列ベースのデータ用のシンプルな非同期チャネルが必要な場合は、このメソッドを使用します。

addJavascriptInterface を使用する(レガシー)

最も古い方法は、ネイティブ オブジェクト インスタンスを WebView に直接挿入する方法です。

仕組み: Kotlin または Java のクラスを定義し、許可するメソッドに @JavascriptInterface アノテーションを付け、addJavascriptInterface(Object, String) を使用してクラスのインスタンスを WebView に追加します。

特徴:

  • 同期: Android コードのメソッドが戻るまで、JavaScript 実行環境がブロックされます。
  • スレッド安全性: システムはバックグラウンド スレッドでメソッドを呼び出すため、Kotlin または Java 側で慎重な同期が必要です。
  • セキュリティ リスク: デフォルトでは、addJavascriptInterface は WebView 内のすべてのフレーム(iframe を含む)で使用できます。オリジンベースのアクセス制御がありません。WebView の非同期動作により、インターフェースを呼び出すフレームの URL を安全に特定することはできません。WebView.getUrl() などのメソッドは、正確性が保証されておらず、リクエストを行った特定のフレームを示すものでもないため、セキュリティ検証に利用してはなりません。

メカニズムの概要

次の表は、3 つの主なネイティブ ブリッジ実装メカニズムを簡単に比較したものです。

メソッド addWebMessageListener postWebMessage addJavascriptInterface
実装 非同期(メインスレッドのリスナー) アシンクロナス シンクロナス
セキュリティ 最上位(許可リストに基づく) 高(オリジン認識) 低(オリジン チェックなし)
複雑さ 簡単
方向 双方向 双方向 Web to App
WebView の最小バージョン バージョン 82(および Jetpack Webkit 1.3.0) バージョン 45(および Jetpack Webkit 1.1.0) すべてのバージョン
推奨 はい いいえ ×

大規模なデータ転送を処理する

マルチメガバイトの文字列やバイナリ ファイルなどの大きなペイロードを転送する場合は、メモリを慎重に管理して、32 ビット デバイスでのアプリケーション応答なし(ANR)エラーやクラッシュを回避する必要があります。このセクションでは、ホストアプリとウェブ コンテンツ間で大量のデータを転送する際のさまざまな手法と制限事項について説明します。

バイト配列でバイナリデータを転送する

WebMessageCompat クラスを使用すると、バイナリデータを Base64 文字列にシリアル化する代わりに、byte[] 配列を直接送信できます。Base64 ではデータサイズに約 33% のオーバーヘッドが追加されるため、この方法はメモリ効率が大幅に向上し、処理速度も速くなります。

  • バイナリの利点: 画像ファイルや音声などのバイナリデータをネイティブ アプリとウェブ コンテンツ間で転送します。
  • 制限事項: バイト配列を使用した場合でも、システムは、アプリと WebView がウェブ コンテンツのレンダリングに使用する分離プロセス間のプロセス間通信(IPC)境界を越えてデータをコピーします。ただし、非常に大きなファイルの場合、依然として大量のメモリが消費されます。

次のコード例は、WebMessageCompat.TYPE_ARRAY_BUFFER でマークされたメッセージを受信し、WebViewFeature.MESSAGE_ARRAY_BUFFER をチェックしてバイナリデータで任意に返信するよう、ネイティブ アプリ側で addWebMessageListener を設定する方法を示しています。

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
      );
  }
}

次の JavaScript コードは、addWebMessageListener のクライアントサイド実装を示しています。これにより、ウェブ コンテンツは、前の例で挿入された window.myBridge プロキシを使用して、ネイティブ アプリとの間でバイナリデータ(ArrayBuffer)を送受信できます。

// 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]);
        }
    };
}

効率的な大規模データ読み込み

非常に大きなファイル(10 MB 超)の場合は、shouldInterceptRequest メソッドを使用してデータをストリーミングします。

  1. ウェブページが、カスタムのプレースホルダ URL への fetch() 呼び出しを開始します。例: https://app.local/large-file
  2. Android アプリは、WebViewClient.shouldInterceptRequest でこのリクエストをインターセプトします。
  3. アプリはデータを InputStream として返します。

これにより、ペイロード全体を一度にメモリに読み込むのではなく、データをチャンク単位でストリーミングできます。

次の JavaScript 関数は、カスタムのプレースホルダ URL への標準の fetch() 呼び出しを使用して、ネイティブ アプリケーションから大きなバイナリファイルを効率的に読み込むためのクライアントサイド コードを示しています。

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);
    }
}

次のコード例は、Kotlin と Java の両方で WebViewClient.shouldInterceptRequest メソッドを使用して、ウェブ コンテンツからリクエストされたカスタム プレースホルダ URL をインターセプトし、大きなバイナリ ファイルをストリーミングするネイティブ アプリ側の処理を示しています。

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);
  }
});

セキュリティに関する推奨事項に沿って対応する

アプリケーションとユーザーデータを保護するには、ブリッジを実装する際に次のガイドラインに従ってください。

  • HTTPS を適用: 悪意のあるサードパーティ コンテンツがアプリのネイティブ ロジックを呼び出せないように、安全なオリジンとの通信のみを許可します。

  • オリジンルールに依存する: 信頼を扱う最善の方法は、allowedOriginRules を厳密に定義し、メッセージ コールバックで提供される sourceOrigin を確認することです。絶対に必要な場合を除き、すべてのオリジンに一致する完全なワイルドカード(*)を唯一のオリジンルールとして使用しないでください。サブドメイン(*.example.com など)にワイルドカードを使用すると、複数のサブドメイン(foo.example.combar.example.com など)を一致させる場合に有効かつ安全です。

    : オリジン ルールは悪意のあるサードパーティのウェブサイトや非表示の iframe から保護しますが、信頼できるドメイン内のクロスサイト スクリプティング(XSS)の脆弱性から保護することはできません。たとえば、ウェブページにユーザー作成コンテンツが表示され、保存型 XSS の脆弱性がある場合、攻撃者は信頼できるオリジンとして機能するスクリプトを実行できます。機密性の高いネイティブ プラットフォーム オペレーションを実行する前に、メッセージ ペイロードに検証を適用することを検討してください。

  • サーフェス領域を最小限に抑える: ウェブページに必要な特定のメソッドまたはデータのみを公開します。

  • 実行時に機能を確認する: addWebMessageListener などの最近のブリッジ API は、Jetpack Webkit ライブラリの一部です。そのため、電話をかける前に、必ず WebViewFeature.isFeatureSupported() を使用してサポートを確認してください。