تتناول هذه الصفحة الطرق المختلفة وأفضل الممارسات لإنشاء
جسر أصلي، يُعرف أيضًا باسم جسر JavaScript، لتسهيل التواصل
بين محتوى الويب في WebView وتطبيق Android مضيف.
يتيح ذلك لمطوّري الويب استخدام JavaScript للوصول إلى ميزات النظام الأساسي الأصلية، مثل الكاميرا أو نظام الملفات أو أجهزة الاستشعار المتقدّمة، والتي لا توفّرها عادةً واجهات برمجة التطبيقات العادية على الويب.
حالات الاستخدام
يتيح تنفيذ جسر JavaScript سيناريوهات تكامل مختلفة حيث يتطلّب محتوى الويب وصولاً أعمق إلى نظام التشغيل Android. في ما يلي بعض الأمثلة:
- عمليات الدمج مع الأنظمة الأساسية: تشغيل مكوّنات واجهة مستخدم Android الأصلية (على سبيل المثال، طلبات المصادقة باستخدام المقاييس الحيوية،
BottomSheetDialog) من صفحة ويب - الأداء: نقل المهام الحسابية المعقّدة إلى رموز Java أو Kotlin الأصلية
- استمرار البيانات: الوصول إلى قواعد البيانات المشفّرة المحلية أو الإعدادات المفضّلة المشترَكة
- عمليات نقل البيانات الكبيرة: تمرير ملفات وسائط أو بنى بيانات معقّدة بين التطبيق وعارض الويب
آليات التواصل
يوفّر نظام التشغيل Android ثلاثة أجيال أساسية من واجهات برمجة التطبيقات لإنشاء جسر أصلي. وعلى الرغم من أنّها لا تزال متاحة، إلا أنّها تختلف بشكل كبير من حيث الأمان وسهولة الاستخدام والأداء.
استخدام addWebMessageListener (يُنصح به)
addWebMessageListener هي الطريقة الأحدث والأكثر فعالية للتواصل بين محتوى الويب ورمز التطبيق الأصلي. ويجمع بين سهولة استخدام واجهة JavaScript وأمان نظام المراسلة.
طريقة العمل: يضيف التطبيق متتبِّعًا يحمل اسمًا معيّنًا ومجموعة من قواعد المصدر المسموح به. بعد ذلك، يتأكّد WebView من أنّ كائن JavaScript متوفّر في النطاق العام (window.objectName) منذ لحظة بدء تحميل الصفحة.
الإعداد: لضمان أنّ WebView يُدخل كائن JavaScript قبل تنفيذ أي نص برمجي، عليك استدعاء addWebMessageListener قبل استدعاء loadUrl().
الميزات الأساسية:
الأمان والثقة: على عكس واجهات برمجة التطبيقات القديمة، تتطلّب هذه الطريقة
Set<String>منallowedOriginRulesأثناء عملية الإعداد. وهي الآلية الأساسية لتحديد مستوى الثقة.عند تحديد مصدر موثوق به، مثل
https://example.com، يضمن WebView أنّه لا يعرض كائنات JavaScript التي تم إدخالها إلا لصفحات الويب التي تم تحميلها من هذا المصدر تحديدًا.تتلقّى دالة رد الاتصال الخاصة بمعالج الأحداث الأصلي المَعلمة
sourceOriginمع كل رسالة. يمكنك استخدام هذا الحقل للتحقّق من المصدر الدقيق للمرسِل إذا كان الجسر يتيح مصادر متعددة مسموح بها.بما أنّ WebView تفرض عمليات التحقّق من المصدر هذه بشكل صارم على مستوى النظام الأساسي، يمكن لتطبيقك بشكل عام الاعتماد على الرسائل الواردة من
sourceOriginموثوق على أنّها صحيحة، ما يلغي الحاجة إلى التحقّق الدقيق من الحمولة في معظم عمليات التنفيذ العادية.- تطابق WebView القواعد مع المخطط (HTTP/HTTPS) والمضيف والمنفذ.
- يتجاهل WebView المسارات. على سبيل المثال، يسمح
https://example.comباستخدامhttps://example.com/loginوhttps://example.com/home. - يقتصر استخدام أحرف البدل في WebView على بداية المضيف للنطاقات الفرعية. على سبيل المثال، يتطابق
https://*.example.comمعhttps://foo.example.comولكن ليس معhttps://example.com. إذا كنت بحاجة إلى مطابقة كل منhttps://example.comونطاقاته الفرعية، يجب إضافة كل قاعدة مصدر بشكل منفصل إلى القائمة المسموح بها (على سبيل المثال،"https://example.com", "https://*.example.com"). ولا يمكنك استخدام أحرف البدل للمخطط أو في منتصف النطاق.
يؤدي ذلك إلى حصر استخدام الجسر في النطاقات التي تم التحقّق منها، ما يمنع تنفيذ الرمز البرمجي الأصلي من خلال محتوى تابع لجهة خارجية غير مصرَّح به أو إطارات iframe تم إدخالها.
إتاحة استخدام إطارات متعددة: تعمل هذه الميزة على مستوى جميع الإطارات التي تتطابق مع قواعد المصدر.
إنشاء سلاسل محادثات: يتم تنفيذ دالة معاودة الاتصال الخاصة بمعالج الأحداث في سلسلة المحادثات الرئيسية (واجهة المستخدم) للتطبيق. إذا كان الجسر يحتاج إلى معالجة بيانات معقّدة أو تحليل JSON أو عمليات بحث في قاعدة البيانات، عليك نقل هذه العمليات إلى سلسلة محادثات في الخلفية لمنع تجميد واجهة مستخدم التطبيق بسبب الخطأ "التطبيق لا يستجيب" (ANR).
ثنائي الاتجاه: عندما ترسل صفحة الويب رسالة، يتلقّى التطبيق
JavaScriptReplyProxyيمكنه استخدامه لإرسال رسائل إلى هذا الإطار المحدّد. يمكنك الاحتفاظ بهذا العنصرreplyProxyواستخدامه في أي وقت لإرسال أي عدد من الرسائل إلى الصفحة، وليس فقط للرد على كل رسالة فردية ترسلها الصفحة. إذا انتقل الإطار الأصلي إلى صفحة أخرى أو تم إتلافه، سيتم تجاهل الرسائل المُرسَلة باستخدامpostMessage()على الخادم الوكيل بدون إشعار.بدء العملية من التطبيق: على الرغم من أنّه يجب أن تبدأ صفحة الويب دائمًا قناة التواصل مع التطبيق، يمكن للتطبيق الأصلي أن يطلب من صفحة الويب بشكل أحادي الجانب بدء هذه العملية. يمكن للتطبيق الأصلي التواصل مع صفحة الويب باستخدام
addDocumentStartJavaScript()(لتقييم JavaScript قبل تحميل الصفحة) أوevaluateJavaScript()(لتقييم JavaScript بعد تحميل الصفحة).
قيد: ترسل واجهة برمجة التطبيقات هذه البيانات كسلاسل أو مصفوفات byte[]. بالنسبة إلى بنى البيانات الأكثر تعقيدًا، مثل عناصر JSON، يجب تحويلها إلى تسلسل بتنسيق من هذه التنسيقات، ثم تحويلها إلى تسلسل عكسي على الجانب الآخر لإعادة إنشاء بنية البيانات.
مثال على الاستخدام:
لفهم التسلسل الكامل لعملية تبادل الرسائل الثنائية الاتجاه، يتم تنفيذ الأحداث بالترتيب التالي:
- البدء (التطبيق): يسجّل التطبيق الأصلي متتبِّع الأحداث باستخدام
addWebMessageListenerويحمّل صفحة الويب باستخدامloadUrl(). - إرسال الرسالة (على الويب): تطلب JavaScript على صفحة الويب تنفيذ
myObject.postMessage(message)لبدء عملية التواصل. - تلقّي الرسائل والردّ عليها (التطبيق): يتلقّى التطبيق الرسالة في دالة معاودة الاتصال الخاصة بالمستمع ويردّ عليها باستخدام
replyProxy.postMessage()المقدَّم. - تلقّي الردّ (على الويب): تتلقّى صفحة الويب الردّ غير المتزامن في دالّة رد الاتصال
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 وتمرير أحد منافذه مع الرسالة إلى محتوى الويب.
الخصائص:
- غير متزامن: على غرار
addWebMessageListener، تستخدم هذه الطريقة المراسلة غير المتزامنة، ما يضمن استجابة صفحة الويب لتفاعلات المستخدمين أثناء معالجة التطبيق للبيانات في الخلفية. - إدراك المصدر: يمكنك تحديد
targetOriginلضمان أنّ WebView لا يرسل البيانات إلا إلى موقع إلكتروني موثوق به.
القيود:
- النطاق: تقتصر هذه الواجهة على التواصل مع الإطار الرئيسي. ولا يتيح توجيه الرسائل أو إرسالها مباشرةً إلى إطارات iframe.
- قيود معرّف الموارد المنتظم (URI): لا يمكنك استخدام هذه الطريقة للمحتوى الذي يتم تحميله باستخدام معرّفات الموارد المنتظمة
data:أوfile:أوloadData()، ما لم تحدّد "*" كمعرّف الموارد المنتظم المستهدف. يؤدي ذلك إلى السماح لأي صفحة بتلقّي الرسالة. - مخاطر الهوية: لا تتوفّر طريقة واضحة للمحتوى على الويب للتحقّق من هوية المرسِل. قد تكون الرسالة التي تتلقّاها صفحة الويب قد نشأت من تطبيقك الأصلي أو إطار iframe آخر.
استخدِم هذه الطريقة عندما تحتاج إلى قناة بسيطة وغير متزامنة للبيانات المستندة إلى السلاسل في إصدارات Android القديمة التي لا تتوافق مع addWebMessageListener.
استخدام addJavascriptInterface (الإصدار القديم)
تتضمّن الطريقة الأقدم إدخال مثيل كائن أصلي مباشرةً في WebView.
طريقة العمل: عليك تحديد فئة Kotlin أو Java، وإضافة التعليقات التوضيحية @JavascriptInterface إلى الطرق المسموح بها، وإضافة مثيل للفئة إلى WebView باستخدام addJavascriptInterface(Object, String).
الخصائص:
- متزامن: تحظر بيئة تنفيذ JavaScript إلى أن تعرض الطريقة في رمز Android.
- أمان سلاسل التنفيذ: يستدعي النظام طرقًا في سلسلة تنفيذ في الخلفية، ما يتطلّب مزامنة دقيقة على مستوى Kotlin أو Java.
- مخاطر الأمان: يتوفّر
addJavascriptInterfaceتلقائيًا لكل إطار ضمن WebView، بما في ذلك الإطارات المضمّنة (iframe). ولا يتضمّن ميزة التحكّم في الوصول استنادًا إلى المصدر. بسبب السلوك غير المتزامن لـ WebView، لا يمكن تحديد عنوان URL للإطار الذي يستدعي واجهتك بأمان. يجب عدم الاعتماد على طرق مثلWebView.getUrl()للتحقّق من الأمان، لأنّه لا يمكن ضمان دقّتها ولا تشير إلى الإطار المحدّد الذي أرسل الطلب.
ملخّص الآليات
يقدّم الجدول التالي مقارنة سريعة لآليات التنفيذ الأساسية الثلاث لجسر التطبيق الأصلي:
| الطريقة | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| التنفيذ | غير متزامن (متتبِّع في سلسلة التعليمات الرئيسية) | بدون تزامن | متزامن |
| الأمان | الأعلى (استنادًا إلى القائمة المسموح بها) | عالية (تتضمّن معلومات المصدر) | منخفضة (بدون عمليات تحقّق من المصدر) |
| التعقيد | متوسط | متوسط | بسيط |
| توجيه المشاهدين لاتخاذ إجراء | ثنائي الاتجاه | ثنائي الاتجاه | من الويب إلى التطبيق |
| الحدّ الأدنى لإصدار WebView | الإصدار 82 (وJetpack Webkit 1.3.0) | الإصدار 45 (وJetpack Webkit 1.1.0) | جميع النُسخ |
| ننصح به | نعم | لا | لا |
التعامل مع عمليات نقل البيانات الكبيرة
يجب إدارة الذاكرة بعناية عند نقل حمولات كبيرة، مثل السلاسل التي تبلغ عدة ميغابايت أو الملفات الثنائية، لتجنُّب أخطاء "التطبيق لا يستجيب" (ANR) أو الأعطال على الأجهزة التي تعمل بنظام 32 بت. يناقش هذا القسم التقنيات المختلفة والقيود المرتبطة بنقل كميات كبيرة من البيانات بين التطبيق المضيف ومحتوى الويب.
نقل البيانات الثنائية باستخدام مصفوفات البايت
باستخدام الفئة WebMessageCompat، يمكنك إرسال مصفوفات byte[] مباشرةً
بدلاً من تحويل البيانات الثنائية إلى سلاسل Base64. بما أنّ Base64 يضيف حوالي% 33 من الحمل الزائد إلى حجم البيانات، يكون ذلك أكثر كفاءة في استخدام الذاكرة وأسرع بكثير.
- ميزة البيانات الثنائية: يمكنك نقل البيانات الثنائية، مثل ملفات الصور أو الصوت، بين تطبيقك الأصلي ومحتوى الويب.
- القيود: حتى مع استخدام مصفوفات البايت، ينسخ النظام البيانات عبر حدود الاتصال بين العمليات (IPC) بين التطبيق والعملية المعزولة التي يستخدمها WebView لعرض محتوى الويب. سيظل هذا الإجراء يستهلك مساحة كبيرة من الذاكرة للملفات الكبيرة جدًا.
توضّح أمثلة الرموز البرمجية التالية كيفية إعداد addWebMessageListener على جانب التطبيق الأصلي لتلقّي الرسائل التي تحمل العلامة WebMessageCompat.TYPE_ARRAY_BUFFER والردّ اختياريًا ببيانات ثنائية من خلال التحقّق من 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
);
}
}
يوضّح رمز JavaScript التالي عملية التنفيذ من جهة العميل لـ addWebMessageListener، ما يتيح لمحتوى الويب إرسال البيانات الثنائية (ArrayBuffer) واستلامها من التطبيق الأصلي وإليه باستخدام وكيل window.myBridge الذي تم إدراجه في المثال السابق.
// 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 ميغابايت)، استخدِم طريقة shouldInterceptRequest لبث البيانات:
- تبدأ صفحة الويب طلب
fetch()إلى عنوان URL مخصّص للنائب. على سبيل المثال،https://app.local/large-file. - يعترض تطبيق Android هذا الطلب في
WebViewClient.shouldInterceptRequest. - يعرض التطبيق البيانات على شكل
InputStream.
يتيح ذلك بث البيانات في أجزاء بدلاً من تحميل الحمولة الكاملة في الذاكرة دفعة واحدة.
توضّح دالة JavaScript التالية الرمز البرمجي من جهة العميل لتحميل ملف ثنائي كبير بكفاءة من التطبيق الأصلي باستخدام طلب fetch() عادي إلى عنوان 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);
}
}
توضّح أمثلة الرموز البرمجية التالية جانب التطبيق الأصلي، باستخدام الطريقة WebViewClient.shouldInterceptRequest في كل من Kotlin وJava، لبث ملف ثنائي كبير عن طريق اعتراض عنوان 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.comوbar.example.com).ملاحظة: على الرغم من أنّ قواعد المصدر تحمي من المواقع الإلكترونية الضارة التابعة لجهات خارجية ومن إطارات iframe المخفية، إلا أنّها لا تحمي من ثغرات النصوص البرمجية للمواقع المتقاطعة (XSS) ضمن نطاقك الموثوق به. على سبيل المثال، إذا كانت صفحة الويب تعرض محتوًى من إنشاء المستخدمين وكانت عرضة لهجمات البرمجة عبر المواقع المخزّنة، يمكن للمهاجم تنفيذ برنامج نصي يعمل كمصدر موثوق به. ننصحك بتطبيق عملية التحقّق من صحة حمولات الرسائل قبل تنفيذ عمليات حساسة على النظام الأساسي.
تقليل مساحة العرض: لا تعرض إلا الطرق أو البيانات المحدّدة التي تحتاج إليها صفحة الويب.
التحقّق من الميزات في وقت التشغيل: تشكّل واجهات برمجة التطبيقات الحديثة للربط، بما في ذلك
addWebMessageListener، جزءًا من مكتبة Jetpack Webkit. لذلك، عليك دائمًا التحقّق من توفّر الدعم باستخدامWebViewFeature.isFeatureSupported()قبل الاتصال بهم.