Mengakses API native dengan jembatan JavaScript

Halaman ini membahas berbagai metode dan praktik terbaik untuk membuat a bridge native, yang juga dikenal sebagai bridge JavaScript, guna memfasilitasi komunikasi antara konten web di WebView dan aplikasi Android host.

Hal ini memungkinkan developer web menggunakan JavaScript untuk mengakses fitur platform native—seperti kamera, sistem file, atau sensor hardware canggih—yang biasanya tidak disediakan oleh API web standar.

Kasus penggunaan

Implementasi bridge JavaScript memungkinkan berbagai skenario integrasi saat konten web memerlukan akses yang lebih mendalam ke sistem operasi Android. Berikut beberapa contohnya:

  • Integrasi platform: Memicu komponen UI Android native (misalnya, perintah Biometrik, BottomSheetDialog) dari halaman web.
  • Performa: Mengalihkan tugas komputasi berat ke kode Java atau Kotlin native.
  • Persistensi data: Mengakses database terenkripsi lokal atau preferensi bersama.
  • Transfer data besar: Meneruskan file media atau struktur data kompleks antara aplikasi dan perender web.

Mekanisme komunikasi

Android menawarkan tiga generasi API utama untuk membuat bridge native. Meskipun semuanya masih tersedia, ketiganya memiliki perbedaan yang signifikan dalam hal keamanan, kegunaan, dan performa.

Menggunakan addWebMessageListener (Direkomendasikan)

addWebMessageListener adalah pendekatan yang paling modern dan direkomendasikan untuk komunikasi antara konten web dan kode aplikasi native. Metode ini menggabungkan kemudahan penggunaan antarmuka JavaScript dengan keamanan sistem pesan.

Cara kerjanya: Aplikasi menambahkan pemroses dengan nama tertentu dan sekumpulan aturan asal yang diizinkan. WebView kemudian memastikan objek JavaScript ada dalam cakupan global (window.objectName) sejak halaman mulai dimuat.

Inisialisasi: Untuk memastikan WebView menyuntikkan objek JavaScript sebelum skrip apa pun berjalan, Anda harus memanggil addWebMessageListener sebelum membuka halaman (seperti memanggil WebViewCompat.navigate atau loadUrl).

Fitur utama:

  • Keamanan dan kepercayaan: Tidak seperti API lama, metode ini memerlukan Set<String> dari allowedOriginRules selama inisialisasi. Ini adalah mekanisme utama untuk membangun kepercayaan.

    Saat Anda menentukan asal tepercaya, seperti https://example.com, WebView menjamin bahwa WebView hanya mengekspos objek JavaScript yang disuntikkan ke halaman web yang dimuat dari asal yang sama.

    Callback pemroses native menerima parameter sourceOrigin dengan setiap pesan. Anda dapat menggunakannya untuk memverifikasi asal pengirim yang tepat jika bridge Anda mendukung beberapa asal yang diizinkan.

    Karena WebView secara ketat menerapkan pemeriksaan asal ini di tingkat platform, aplikasi Anda umumnya dapat mengandalkan pesan yang diterima dari sourceOrigin tepercaya sebagai pesan yang benar, sehingga tidak perlu validasi payload yang ketat di sebagian besar implementasi standar.

    • WebView mencocokkan aturan dengan skema (HTTP/HTTPS), host, dan port.
    • WebView mengabaikan jalur. Misalnya, https://example.com mengizinkan https://example.com/login dan https://example.com/home.
    • WebView membatasi karakter pengganti secara ketat ke awal host untuk subdomain. Misalnya, https://*.example.com cocok dengan https://foo.example.com, tetapi tidak cocok dengan https://example.com. Jika Anda perlu mencocokkan https://example.com dan subdomainnya, Anda harus menambahkan setiap aturan asal secara terpisah ke daftar yang diizinkan (misalnya, "https://example.com", "https://*.example.com"). Anda tidak dapat menggunakan karakter pengganti untuk skema atau di tengah domain.

    Hal ini membatasi bridge ke domain yang terverifikasi, sehingga mencegah konten pihak ketiga yang tidak sah atau iframe yang disuntikkan menjalankan kode native.

  • Dukungan multi-frame: Berfungsi di semua frame yang cocok dengan aturan asal rules.

  • Threading: Callback pemroses berjalan di thread utama (UI) aplikasi. Jika bridge Anda perlu menangani pemrosesan data yang kompleks, penguraian JSON, atau pencarian database, Anda harus mengalihkan pekerjaan tersebut ke thread latar belakang untuk mencegah UI aplikasi membeku dengan error "aplikasi tidak merespons" (ANR).

  • Dua arah: Saat halaman web mengirim pesan, aplikasi akan menerima JavaScriptReplyProxy yang dapat digunakan untuk mengirim pesan kembali ke frame tertentu. Anda dapat mempertahankan objek replyProxy ini dan menggunakannya kapan saja untuk mengirim pesan dalam jumlah berapa pun ke halaman, bukan hanya untuk membalas setiap pesan yang dikirim halaman. Jika frame asal berpindah atau dihancurkan, pesan yang dikirim menggunakan postMessage() di proxy akan diabaikan secara diam-diam.

  • Inisiasi sisi aplikasi: Meskipun halaman web harus selalu memulai saluran komunikasi dengan aplikasi, aplikasi native dapat secara sepihak meminta halaman web untuk memulai proses ini. Aplikasi native dapat berkomunikasi dengan halaman web menggunakan addDocumentStartJavaScript() (untuk mengevaluasi JavaScript sebelum halaman dimuat) atau evaluateJavaScript() (untuk mengevaluasi JavaScript setelah halaman dimuat).

Batasan: API ini mengirim data sebagai string atau array byte[]. Untuk struktur data yang lebih rumit, seperti objek JSON, Anda harus melakukan serialisasi ke salah satu format tersebut, lalu melakukan deserialisasi di sisi lain untuk merekonstruksi struktur data.

Contoh penggunaan:

Untuk memahami urutan lengkap pertukaran pesan dua arah, peristiwa akan berjalan dalam urutan ini:

  1. Inisiasi (aplikasi): Aplikasi native mendaftarkan pemroses dengan addWebMessageListener dan memulai navigasi halaman (seperti dengan WebViewCompat.navigate atau loadUrl).
  2. Pengiriman pesan (web): JavaScript halaman web memanggil myObject.postMessage(message) untuk memulai komunikasi.
  3. Penerimaan dan balasan pesan (aplikasi): Aplikasi menerima pesan dalam callback pemroses dan membalas menggunakan replyProxy.postMessage() yang disediakan.
  4. Penerimaan balasan (web): Halaman web menerima balasan asinkron dalam fungsi callback 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 berikut menunjukkan implementasi sisi klien dari addWebMessageListener, yang memungkinkan konten web menerima pesan dari aplikasi native dan mengirim pesannya sendiri melalui proxy myObject.

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

Menggunakan postWebMessage (Alternatif)

Android memperkenalkan metode ini untuk menyediakan alternatif berbasis pesan asinkron yang mirip dengan window.postMessage web.

Cara kerjanya: Aplikasi menggunakan WebViewCompat.postWebMessage untuk mengirim payload ke frame utama halaman web. Untuk membuat saluran komunikasi dua arah, Anda dapat membuat WebMessageChannel dan meneruskan salah satu port-nya dengan pesan ke konten web.

Karakteristik:

  • Asinkron: Seperti addWebMessageListener, metode ini menggunakan pesan asinkron, yang memastikan halaman web tetap responsif terhadap interaksi pengguna saat aplikasi memproses data di latar belakang.
  • Mengetahui asal: Anda dapat menentukan targetOrigin untuk memastikan WebView hanya mengirimkan data ke situs tepercaya.

Batasan:

  • Cakupan: API ini membatasi komunikasi ke frame utama. API ini tidak mendukung pengiriman pesan atau pengiriman pesan langsung ke iframe.
  • Batasan URI: Anda tidak dapat menggunakan metode ini untuk konten yang dimuat menggunakan data: URI, file: URI, atau loadData(), kecuali jika Anda menentukan "*" sebagai asal target. Dengan melakukannya, halaman mana pun dapat menerima pesan.
  • Risiko identitas: Tidak ada cara yang jelas bagi konten web untuk memverifikasi identitas pengirim. Pesan yang diterima halaman web dapat berasal dari aplikasi native Anda atau iframe lain.

Gunakan metode ini jika Anda memerlukan saluran asinkron sederhana untuk data berbasis string di versi Android sebelumnya yang tidak mendukung addWebMessageListener.

Menggunakan addJavascriptInterface (Lama)

Metode terlama melibatkan penyuntikan instance objek native langsung ke WebView.

Cara kerjanya: Anda menentukan class Kotlin atau Java, memberi anotasi pada metode yang diizinkan dengan @JavascriptInterface, dan menambahkan instance class ke WebView menggunakan addJavascriptInterface(Object, String).

Karakteristik:

  • Sinkron: Lingkungan eksekusi JavaScript diblokir hingga metode dalam kode Android Anda ditampilkan.
  • Keamanan thread: Sistem memanggil metode di thread latar belakang, sehingga memerlukan sinkronisasi yang cermat di sisi Kotlin atau Java.
  • Risiko keamanan: Secara default, addJavascriptInterface tersedia untuk setiap frame dalam WebView, termasuk iframe. Metode ini tidak memiliki kontrol akses berbasis asal. Karena perilaku asinkron WebView, Anda tidak dapat menentukan URL frame yang memanggil antarmuka Anda dengan aman. Anda tidak boleh mengandalkan metode seperti WebView.getUrl() untuk verifikasi keamanan, karena metode tersebut tidak dijamin akurat dan tidak menunjukkan frame tertentu yang membuat permintaan.

Ringkasan mekanisme

Tabel berikut memberikan perbandingan singkat dari tiga mekanisme implementasi bridge native utama:

Metode addWebMessageListener postWebMessage addJavascriptInterface
Implementasi Asinkron (Pemroses di thread utama) Asinkron Sinkron
Keamanan Tertinggi (Berbasis daftar yang diizinkan) Tinggi (Mengetahui asal) Rendah (Tidak ada pemeriksaan asal)
Kompleksitas Sedang Sedang Sederhana
Arah Dua arah Dua arah Web ke aplikasi
Versi WebView minimum Versi 82 (dan Jetpack Webkit 1.3.0) Versi 45 (dan Jetpack Webkit 1.1.0) Semua versi
Direkomendasikan Ya Tidak Tidak

Menangani transfer data besar

Anda harus mengelola memori dengan cermat saat mentransfer payload besar, seperti string multi-megabyte atau file biner, untuk menghindari error atau error Aplikasi Tidak Merespons (ANR) di perangkat 32-bit. Bagian ini membahas berbagai teknik dan batasan yang terkait dengan transfer data dalam jumlah besar antara aplikasi host dan konten web.

Mentransfer data biner dengan array byte

Dengan class WebMessageCompat, Anda dapat mengirim array byte[] secara langsung bukan melakukan serialisasi data biner ke dalam string Base64. Karena Base64 menambahkan overhead sekitar 33% ke ukuran data, hal ini jauh lebih hemat memori dan lebih cepat.

  • Keunggulan biner: Transfer data biner seperti file gambar atau audio antara aplikasi native dan konten web.
  • Batasan: Bahkan dengan array byte, sistem menyalin data di seluruh batas komunikasi antar-proses (IPC) antara aplikasi dan proses terisolasi yang digunakan WebView untuk merender konten web. Hal ini masih menggunakan memori yang signifikan untuk file yang sangat besar.

Contoh kode berikut menunjukkan cara menyiapkan addWebMessageListener di sisi aplikasi native untuk menerima pesan yang ditandai dengan WebMessageCompat.TYPE_ARRAY_BUFFER dan secara opsional membalas dengan data biner dengan memeriksa 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
      );
  }
}

Kode JavaScript berikut menunjukkan implementasi sisi klien dari addWebMessageListener, yang memungkinkan konten web mengirim dan menerima data biner (ArrayBuffer) ke dan dari aplikasi native menggunakan proxy window.myBridge yang disuntikkan dalam contoh sebelumnya.

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

Pemuatan data skala besar yang efisien

Untuk file yang sangat besar (>10 MB), gunakan metode shouldInterceptRequest untuk melakukan streaming data:

  1. Halaman web memulai panggilan fetch() ke URL kustom dan placeholder. Misalnya, https://app.local/large-file.
  2. Aplikasi Android melakukan intersepsi permintaan ini di WebViewClient.shouldInterceptRequest.
  3. Aplikasi menampilkan data sebagai InputStream.

Hal ini memungkinkan streaming data dalam potongan, bukan memuat seluruh payload ke dalam memori sekaligus.

Fungsi JavaScript berikut menunjukkan kode sisi klien untuk memuat file biner besar secara efisien dari aplikasi native menggunakan panggilan fetch() standar ke URL kustom dan placeholder.

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

Contoh kode berikut menunjukkan sisi aplikasi native, menggunakan metode WebViewClient.shouldInterceptRequest di Kotlin dan Java, untuk melakukan streaming file biner besar dengan melakukan intersepsi URL placeholder kustom yang diminta oleh konten web.

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

Mengikuti rekomendasi keamanan

Untuk melindungi aplikasi dan data pengguna, ikuti panduan ini saat menerapkan bridge:

  • Menerapkan HTTPS: Untuk memastikan konten pihak ketiga yang berbahaya tidak dapat memanggil logika native aplikasi Anda, hanya izinkan komunikasi dengan asal yang aman.

  • Mengandalkan aturan asal: Cara terbaik untuk menangani kepercayaan adalah dengan menentukan allowedOriginRules secara ketat dan memeriksa sourceOrigin yang disediakan dalam callback pesan. Hindari penggunaan karakter pengganti penuh (*), yang cocok dengan semua asal, sebagai satu-satunya aturan asal kecuali jika benar-benar diperlukan. Penggunaan karakter pengganti untuk subdomain (misalnya, *.example.com) tetap valid dan aman untuk mencocokkan beberapa subdomain (misalnya, foo.example.com, bar.example.com).

    Catatan: Meskipun aturan asal melindungi dari situs pihak ketiga yang berbahaya dan iframe tersembunyi, aturan tersebut tidak dapat melindungi dari kerentanan pembuatan skrip lintas situs (XSS) dalam domain tepercaya Anda sendiri. Misalnya, jika halaman web Anda menampilkan konten buatan pengguna dan rentan terhadap XSS tersimpan, penyerang dapat menjalankan skrip yang bertindak sebagai asal tepercaya Anda. Pertimbangkan untuk menerapkan validasi ke payload pesan sebelum menjalankan operasi platform native yang sensitif.

  • Meminimalkan area permukaan: Hanya mengekspos metode atau data tertentu yang diperlukan halaman web.

  • Memeriksa fitur saat runtime: API bridge terbaru, termasuk addWebMessageListener, adalah bagian dari library Jetpack Webkit. Jadi, selalu periksa dukungan menggunakan WebViewFeature.isFeatureSupported() sebelum memanggilnya.