الوصول إلى واجهات برمجة التطبيقات الأصلية باستخدام JavaScript bridge

تناقش هذه الصفحة الطرق المختلفة وأفضل الممارسات لإنشاء جسر أصلي، يُعرف أيضًا باسم جسر 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، عليك تسلسلها إلى أحد هذَين التنسيقَين ثم إلغاء تسلسلها على الجانب الآخر لإعادة إنشاء هيكل البيانات.

مثال الاستخدام:

لفهم التسلسل الكامل لعملية تبادل الرسائل ثنائية الاتجاه، تتّبع الأحداث هذا الترتيب:

  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 وتمرير أحد منافذه مع الرسالة إلى محتوى الويب.

الخصائص:

  • غير متزامن: على غرار 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 لبث البيانات:

  1. تبدأ صفحة الويب استدعاء fetch() إلى عنوان URL مخصّص للنائب. على سبيل المثال، https://app.local/large-file.
  2. يعترض تطبيق Android هذا الطلب في WebViewClient.shouldInterceptRequest.
  3. يعرض التطبيق البيانات كـ 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) ضمن نطاقك الموثوق به. على سبيل المثال، إذا كانت صفحة الويب تعرض محتوى من إنشاء المستخدمين ومعرَّضة لهجمات XSS المخزّنة، يمكن للمهاجم تنفيذ نص برمجي يعمل كمصدرك الموثوق به. ننصحك بتطبيق عملية التحقّق من صحة حمولات الرسائل قبل تنفيذ عمليات النظام الأساسي الأصلي الحساسة.

  • تقليل مساحة العرض: لا تعرض إلا الطرق أو البيانات المحدّدة التي تتطلّبها صفحة الويب.

  • التحقّق من الميزات في وقت التشغيل: تُعدّ واجهات برمجة تطبيقات الجسر الحديثة، بما في ذلك addWebMessageListener، جزءًا من مكتبة Jetpack Webkit. لذلك، تحقَّق دائمًا من التوافق باستخدام WebViewFeature.isFeatureSupported() قبل استدعائها.