حقن كود JavaScript في WebView أندرويد بطريقتين
طريقتان للتواصل مع WebView أندرويد من Kotlin — evaluateJavascript للنداءات باتجاه واحد نحو الصفحة، وJavascriptInterface لتستدعي الصفحة كودك في الاتجاه المعاكس — كلاهما مطبَّق في تطبيق يعمل فعلًا.
إتقان إرسال البيانات إلى WebView أندرويد واستقبالها منه، وكيف نتعامل معه.
مطوّرون مشغولون نحن؛ لن أهدر وقتك في مقدمات.
إن احتجت إلى أيٍّ مما يلي:
- إرسال بيانات من أندرويد إلى تطبيق ويبٍ ما.
- استقبال بيانات من تطبيق ويبٍ ما على جهة أندرويد.
فهذه المقالة لك.
إرسال البيانات إلى تطبيق الويب
لنفترض أن لديّ تطبيق ويب أحتاج إلى تمرير بعض البيانات إليه، مثلًا:
- استدعاء دالة 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 عليه — فمن هنا تأتي معظم الثغرات. خذ حذرك، وبرمجة سعيدة.
برمجة سعيدة