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>allowedOriginRulesselama 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
sourceOrigindengan 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
sourceOrigintepercaya 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.commengizinkanhttps://example.com/logindanhttps://example.com/home. - WebView membatasi karakter pengganti secara ketat di awal host untuk
subdomain. Misalnya,
https://*.example.comcocok denganhttps://foo.example.com, tetapi tidak cocok denganhttps://example.com. Jika Anda perlu mencocokkanhttps://example.comdan 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
JavaScriptReplyProxyyang dapat digunakan untuk mengirim pesan kembali ke frame tertentu tersebut. Anda dapat mempertahankan objekreplyProxyini 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 menggunakanpostMessage()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) atauevaluateJavaScript()(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:
- Inisiasi (aplikasi): Aplikasi native mendaftarkan pemroses dengan
addWebMessageListenerdan memuat halaman web denganloadUrl(). - Pengiriman pesan (web): JavaScript halaman web memanggil
myObject.postMessage(message)untuk memulai komunikasi. - Penerimaan dan balasan pesan (aplikasi): Aplikasi menerima pesan di callback
listener dan membalas menggunakan
replyProxy.postMessage()yang diberikan. - 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
targetOriginuntuk 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:, URIfile:, atauloadData(), 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,
addJavascriptInterfacetersedia 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 sepertiWebView.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:
- Halaman web memulai panggilan
fetch()ke URL placeholder kustom. Misalnya,https://app.local/large-file. - Aplikasi Android mencegat permintaan ini di
WebViewClient.shouldInterceptRequest. - 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
allowedOriginRulessecara ketat dan memeriksasourceOriginyang 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 menggunakanWebViewFeature.isFeatureSupported()sebelum menelepon mereka.