Mengakses API native dengan jembatan JavaScript

Halaman ini membahas berbagai metode dan praktik terbaik untuk membuat jembatan native, yang juga dikenal sebagai jembatan JavaScript, untuk 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 jembatan JavaScript memungkinkan berbagai skenario integrasi di mana konten web memerlukan akses yang lebih dalam ke sistem operasi Android. Berikut beberapa contohnya:

  • Integrasi platform: Memicu komponen UI Android native (misalnya, dialog Biometrik, BottomSheetDialog) dari halaman web.
  • Performa: Menurunkan 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 yang kompleks antara aplikasi dan perender web.

Mekanisme komunikasi

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

Gunakan addWebMessageListener (Direkomendasikan)

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

Cara kerjanya: Aplikasi menambahkan pemroses dengan nama tertentu dan serangkaian aturan asal yang diizinkan. Kemudian, WebView 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 memanggil loadUrl().

Fitur utama:

  • Keamanan dan kepercayaan: Tidak seperti API lama, metode ini memerlukan Set<String> 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 tepat tersebut.

    Callback pemroses native menerima parameter sourceOrigin dengan setiap pesan. Anda dapat menggunakan ini untuk memverifikasi asal persis pengirim jika jembatan 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 benar, sehingga tidak perlu validasi payload yang ketat dalam sebagian besar penerapan 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 di 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 jembatan ke domain terverifikasi, sehingga mencegah konten pihak ketiga yang tidak sah atau iframe yang disisipkan agar tidak mengeksekusi kode native.

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

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

  • Bidireksional: Saat halaman web mengirim pesan, aplikasi menerima JavaScriptReplyProxy yang dapat digunakan untuk mengirim pesan kembali ke frame tertentu tersebut. Anda dapat mempertahankan objek replyProxy ini dan menggunakannya kapan saja untuk mengirim sejumlah pesan ke halaman, bukan hanya untuk membalas setiap pesan individual yang dikirim halaman. Jika frame asal keluar atau dihancurkan, pesan yang dikirim menggunakan postMessage() di proxy akan diabaikan tanpa pemberitahuan.

  • 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 dengan 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 berlanjut dalam urutan ini:

  1. Inisiasi (aplikasi): Aplikasi native mendaftarkan pemroses dengan addWebMessageListener dan memuat halaman web dengan loadUrl().
  2. Pengiriman pesan (web): JavaScript halaman web memanggil myObject.postMessage(message) untuk memulai komunikasi.
  3. Penerimaan dan balasan pesan (aplikasi): Aplikasi menerima pesan di callback listener dan membalas menggunakan replyProxy.postMessage() yang diberikan.
  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 penerapan addWebMessageListener di sisi klien, sehingga konten web dapat 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 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.
  • Mengenali asal: Anda dapat menentukan targetOrigin untuk memastikan WebView hanya mengirimkan data ke situs tepercaya.

Batasan:

  • Cakupan: API ini membatasi komunikasi ke frame utama. Fitur ini tidak mendukung pengiriman pesan atau penargetan langsung ke iframe.
  • Batasan URI: Anda tidak dapat menggunakan metode ini untuk konten yang dimuat menggunakan URI data:, URI file:, 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 dalam 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, yang memerlukan sinkronisasi yang cermat di sisi Kotlin atau Java.
  • Risiko keamanan: Secara default, addJavascriptInterface tersedia untuk setiap frame dalam WebView, termasuk iframe. Tidak memiliki kontrol akses berbasis origin. 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 jembatan native utama:

Metode addWebMessageListener postWebMessage addJavascriptInterface
Implementasi Asinkron (Listener di thread utama) Asinkron Sinkron
Keamanan Tertinggi (Berdasarkan daftar yang diizinkan) Tinggi (Menyadari asal) Rendah (Tidak ada pemeriksaan asal)
Kompleksitas Sedang Sedang Sederhana
Arah Bidirectional Bidirectional 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 berukuran besar

Anda harus mengelola memori dengan cermat saat mentransfer payload besar, seperti string multi-megabyte atau file biner, untuk menghindari error Aplikasi Tidak Merespons (ANR) atau error pada 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 menyerialkan data biner ke dalam string Base64. Karena Base64 menambahkan overhead sekitar 33% ke ukuran data, cara ini jauh lebih efisien dan cepat dalam penggunaan memori.

  • Keunggulan biner: Transfer data biner seperti file gambar atau audio antara aplikasi native dan konten web Anda.
  • 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. Cara 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 penerapan sisi klien addWebMessageListener, yang memungkinkan konten web mengirim dan menerima data biner (ArrayBuffer) ke dan dari aplikasi native menggunakan proxy window.myBridge yang disisipkan 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 berskala 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 placeholder kustom. Misalnya, https://app.local/large-file.
  2. Aplikasi Android mencegat 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 placeholder kustom.

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 mencegat 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 Anda, ikuti panduan berikut saat menerapkan jembatan:

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

  • Mengandalkan aturan asal: Cara terbaik untuk menangani kepercayaan adalah dengan menentukan allowedOriginRules secara ketat dan memeriksa sourceOrigin yang diberikan dalam callback pesan. Hindari penggunaan karakter pengganti penuh (*), yang cocok dengan semua situs asal, sebagai satu-satunya aturan asal Anda 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 ini 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 pada payload pesan sebelum menjalankan operasi platform native yang sensitif.

  • Minimalkan area permukaan: Hanya ekspos metode atau data tertentu yang diperlukan halaman web.

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