حقن كود JavaScript في WebView أندرويد بطريقتين

طريقتان للتواصل مع WebView أندرويد من Kotlin — evaluateJavascript للنداءات باتجاه واحد نحو الصفحة، وJavascriptInterface لتستدعي الصفحة كودك في الاتجاه المعاكس — كلاهما مطبَّق في تطبيق يعمل فعلًا.

إتقان إرسال البيانات إلى WebView أندرويد واستقبالها منه، وكيف نتعامل معه.

مطوّرون مشغولون نحن؛ لن أهدر وقتك في مقدمات.

إن احتجت إلى أيٍّ مما يلي:

  1. إرسال بيانات من أندرويد إلى تطبيق ويبٍ ما.
  2. استقبال بيانات من تطبيق ويبٍ ما على جهة أندرويد.

فهذه المقالة لك.

إرسال البيانات إلى تطبيق الويب

لنفترض أن لديّ تطبيق ويب أحتاج إلى تمرير بعض البيانات إليه، مثلًا:

  • استدعاء دالة JavaScript
  • إرسال نص أو كائن JSON

حسنًا، إلى العمل

إنشاء WebView

سأبدأ بتخطيط XML التالي

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HomeFragment">
 
    <WebView
        android:id="@+id/web_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="@string/home_fragment_label"
        app:layout_constraintBottom_toTopOf="@+id/guideline"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_percent=".65" />
 
    <TextView
        android:id="@+id/tvTitle"
        style="@style/TextAppearance.AppCompat.Title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="16dp"
        android:text="@string/we_can_send_data_to_the_web_app"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/web_view" />
 
    <EditText
        android:id="@+id/etMessage"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:autofillHints="name"
        android:hint="@string/message_to_be_sent"
        android:inputType="textAutoComplete"
        android:textColorHint="#757575"
        app:layout_constraintEnd_toStartOf="@+id/btnSend"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvTitle" />
 
    <Button
        android:id="@+id/btnSend"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:text="@string/send"
        app:layout_constraintBottom_toBottomOf="@+id/etMessage"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="@+id/etMessage" />
 
</androidx.constraintlayout.widget.ConstraintLayout>

ولا تنسَ كذلك إضافة تصريح INTERNET

    <uses-permission android:name="android.permission.INTERNET" />

الآن نفعّل JavaScript في الـ WebView ونحمّل صفحتي الخاصة التي أستضيفها على حسابي في GitHub.

binding.webview.apply {
 settings.javaScriptEnabled = true
    loadUrl("https://abd3lraouf.github.io/webview_code_injection_from_android")
}

لنشغّل التطبيق

التطبيق في نسخته الأولى وهو يعمل على جهاز أندرويد

حقن الكود

صار كل شيء جاهزًا، فلنشرع في الحقن

لنفترض أن لديّ دالة JavaScript كهذه:

function messageMe(message) {
  document.getElementById("demo").innerHTML = message;
}

أما الـ DOM فتستطيع توقّعه بسهولة

    <body>
        <h1>Code injection</h1>
        <button>Launch Native Code</button>
        <h2> Messages from Android</h2>
        <p id="demo">Can you replace me?</h2>
    </body>

كل ما علينا فعله ببساطة هو تنفيذ كود JavaScript التالي

messageMe("Message");

لحسن الحظ أن أندرويد يمنحنا وسيلة لتنفيذ كود JavaScript على أي تطبيق ويب نفتحه داخل WebView، عبر الدالة evaluateJavascript المتاحة على الـ WebView نفسه. وكل ما علينا فعله بعدها هو استدعاء هذه الدالة بكود JavaScript مُصاغ صياغةً صحيحة.

evaluateJavascript("messageMe(\"$message\")", null)

لنجرّبها

بهذا انتهى الاتجاه الأول. ننتقل الآن إلى خطوة أعمق: استقبال البيانات من تطبيق الويب.

استقبال البيانات من تطبيق الويب

لنفترض الآن أن لديّ تطبيق ويبًا يريد بدوره أن يوصل بعض البيانات إلى جهة أندرويد، مثل:

  • إرسال نص
  • تشغيل كود أصلي
  • الانتقال إلى شاشة أخرى على جهة أندرويد

ولتنفيذ مثل هذه الميزات علينا ما يلي

إنشاء واجهة JavaScript

واجهة الـ JavaScript في جوهرها ليست إلا واجهة Kotlin تضمّ دالة أو أكثر وُسمت بالوسم @JavascriptInterface، فتصبح هذه الدالة مرئيةً لجهة الـ JavaScript.

لنفترض أنني أريد أن يطلب تطبيق الويب عرض رسالة Toast أصلية

@JavascriptInterface
@Suppress("unused")
fun toast(message: String) {
    Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}

ولكي يستدعي الـ JavaScript هذه الدالة لا بد أن تكون داخل كائنٍ ما؛ فلنستدعِ دالة toast على الكائن JsMediator.

Android = {};
Android.toast = function (message) {
  JsMediator.toast(message);
};

ما فعلته في المقتطف أعلاه: أنشأت كائنًا فارغًا اسمه Android، ثم عرّفت داخله دالة اسمها toast تأخذ معاملًا واحدًا هو message، وهذه الدالة بدورها تستدعي دالة تحمل الاسم نفسه على كائنٍ آخر اسمه JsMediator — وهو الحلقة التي تصل كود الـ JavaScript لدينا بكود الـ Kotlin. وعلى المفتاح JsMediator تحديدًا سأربط واجهة الـ JavaScript. يا سلام.

لا بأس أن تعيد قراءة الفقرة السابقة — أعلم أن استيعابها من أول مرة ليس بالأمر الهين.

/**
* called before start loading the page
*/
fun injectMediator(webView: WebView) {
    val code = "Android = {}; Android.toast = function(message) { JsMediator.toast(message); }"
    webView.evaluateJavascript(code, null)
}

يبقى سؤال واحد: متى نستدعي هذه الدالة؟

ربط الواجهة

وكما هو الحال مع إعداد أي View آخر في أندرويد، يقع هذا الإعداد بعد إنشاء الـ View؛ لذلك سنربط الواجهة في onCreate في الـ Activities، أو في onViewCreated في الـ Fragments.

بهذه الخطوة يعرف عالم الـ JavaScript أن لديّ دالة اسمها toast

binding.webView.apply {
    settings.javaScriptEnabled = true
    setWebContentsDebuggingEnabled(true)
    JavaScriptShareInterface.bind(requireContext(), this)
    loadUrl("https://abd3lraouf.github.io/webview_code_injection_from_android")
}

وتتولى دالة bind إلحاق الواجهة بالمفتاح JsMediator

/**
* called at onCreate to initiate code injection
*/
fun bind(context: Context, webView: WebView) {
    webView.addJavascriptInterface(JavaScriptShareInterface(context), "JsMediator")
}

الخطوة الأخيرة

أريد أن ألتقط خطوة تحميل الصفحة وأحقن عندها الوسيط، كي يمرّر نداءات الـ JavaScript الخاصة بـ Android.toast(message) إلى الوسيط الذي عرّفناه أعلاه.

سننشئ لذلك CustomWebViewClient

object CustomWebViewClient : WebViewClient() {
    override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        JavaScriptShareInterface.injectMediator(view)
    }
}

يحقن هذا الكود الوسيط بعد أن تبدأ الصفحة في التحميل.

وهكذا يصبح كود الإعداد النهائي للـ WebView

binding.webView.apply {
    settings.javaScriptEnabled = true
    setWebContentsDebuggingEnabled(true)
    JavaScriptShareInterface.bind(requireContext(), this)
    webViewClient = CustomWebViewClient
 
    loadUrl("https://abd3lraouf.github.io/webview_code_injection_from_android")
}

لكننا لم ننتهِ بعد.

نحن جميعًا مطوّرو أندرويد صالحون، والصالح منّا يحرّر موارده بعد الاستخدام؛ وهذا الجزء سهل: اربط الواجهة وفُكّ ربطها في دوال دورة حياة الـ View المناسبة.

وطبّقت على الكود شيئًا من مبادئ الـ Clean Code أيضًا. هذا كله وغيره أتركه لك لتكتشفه بنفسك.

هنا أصل إلى نقطة أختم عندها المقال. كل الكود الذي استخدمته في هذا الشرح مستضاف على حسابي في GitHub.

المصادر:

الخلاصة

تحدثت في هذه المقالة عن كيف نسخّر الـ WebView لخدمتنا وكيف نتعامل معه.

انتبه إلى أن الـ WebView سلاحٌ ذو حدّين: علينا الحذر عند استخدامه، بل حتى عند تنفيذ JavaScript عليه — فمن هنا تأتي معظم الثغرات. خذ حذرك، وبرمجة سعيدة.

برمجة سعيدة