بناء جسر اتصال أصيل بين Flutter و WebView باستخدام JavaScript

دقائق القراءة: 7
في عالم تطوير التطبيقات الحديث، أصبحت الحاجة إلى دمج محتوى الويب ضمن التطبيقات الأصيلة أمرًا شائعًا. بعد أن تناولنا سابقًا كيفية بناء جسور الاتصال في بيئات Android و iOS الأصيلة، حان الوقت الآن لنتعمق في كيفية تحقيق هذا التكامل الحيوي ضمن إطار عمل Flutter. قد يبدو الأمر للوهلة الأولى بسيطًا، لكنك ستكتشف قريبًا أنه يتطلب بعض الجهد لإنجاز هذه الوظيفة بكفاءة.

من الأهمية بمكان الإشارة إلى أن Flutter، في وقت كتابة هذا المقال، لا يدعم بشكل مباشر مكونات WebView المدمجة. على عكس التطبيقات الأصيلة المكتوبة بلغات مثل Kotlin أو Swift، حيث يمكنك ببساطة إنشاء مكون WebView جاهز للاستخدام، فإن إضافة هذا المكون إلى تطبيق Flutter يتطلب خطوات إضافية.

يهدف هذا المقال إلى إرشادك خطوة بخطوة حول كيفية تهيئة WebView في تطبيقات Flutter، وكيفية إنشاء جسر اتصال فعال يسمح بتبادل البيانات بسلاسة بين تطبيق Flutter ومحتوى الويب داخل WebView.

تهيئة مكون WebView في تطبيق Flutter

لبدء استخدام WebView في مشروع Flutter، نحتاج أولاً إلى إضافة الحزمة المناسبة. بعد إنشاء مشروع Flutter جديد، سنعتمد على حزمة webview_flutter التي توفر الوظائف اللازمة لدمج وعرض محتوى الويب.

لإضافة هذه التبعية، قم بتعديل ملف pubspec.yaml الخاص بمشروعك على النحو التالي:

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^1.0.7

بعد إضافة التبعية، يجب عليك تشغيل الأمر flutter pub get في الطرفية (Terminal) لتنزيل الحزمة وإضافتها إلى مشروعك:

flutter pub get

الخطوة التالية هي استيراد الحزمة في ملف main.dart الخاص بتطبيقك. هذا سيسمح لك بالوصول إلى الفئات والوظائف التي توفرها الحزمة:

import 'package:webview_flutter/webview_flutter.dart';

إذا لم تكن قد قمت بتنظيف الكود الافتراضي لمشروع Flutter الجديد بعد، فهذا هو الوقت المناسب للقيام بذلك. بعد إزالة جميع التعليقات وزر الإجراء العائم (Floating Action Button) وكل ما يتعلق به، سيبقى لديك الهيكل الأساسي التالي. (لقد أضفت عنصر Text واجهة مستخدم بسيط للعرض فقط):

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Communication Bridge',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Native - JS Communication Bridge'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return Text(
      "Flutter JS-Native Communication Bridge"
    );
  }
}

سينتج عن هذا الكود الواجهة التالية:
شاشة تطبيق Flutter تعرض نص 'Flutter JS-Native Communication Bridge'

إنشاء ملف HTML محلي كأصل (Asset)

نظرًا لأننا سنستخدم ملف HTML محليًا يحتوي على كود JavaScript مدمج، يجب علينا إنشاؤه ضمن هيكل مشروعنا. في تطبيقات Flutter، يجب وضع جميع الأصول المحلية (مثل الصور، الخطوط، وملفات HTML) داخل مجلد يُسمى assets.

لإنشاء مجلد assets، انقر بزر الماوس الأيمن في لوحة المشروع الجانبية اليسرى، ثم اختر New → Directory وقم بتسميته assets.
إنشاء مجلد Assets في مشروع Flutter
هذا هو شكل هيكل الملفات بعد إنشاء مجلد assets:

بعد ذلك، قم بإنشاء ملف باسم index.html داخل مجلد assets وأضف الكود التالي إليه:

<html>
  <head>
    <title>My Local HTML File</title>
  </head>
  <body>
    <h1 id="title">Hello World!</h1>
    <script type="text/javascript">
      function fromFlutter(newTitle) {
        document.getElementById("title").innerHTML = newTitle;
        sendBack();
      }

      function sendBack() {
        messageHandler.postMessage("Hello from JS");
      }
    </script>
  </body>
</html>

ستلاحظ أننا كتبنا دالتين في قسم JavaScript بملف HTML الخاص بنا:

  • fromFlutter: هذه هي الدالة التي سنستدعيها من Flutter مع تمرير نص يمثل العنوان الجديد للصفحة.
  • sendBack: هذه هي الدالة التي سنستدعيها لإعادة التواصل مع Flutter. تقوم هذه الدالة بإرسال رسالة نصية.

سنتطرق إلى تفاصيل محتوى دالة sendBack لاحقًا، ولكن قبل ذلك، يجب علينا إعداد WebView في تطبيقنا.

تذكير هام: لا تنسَ إضافة ملف index.html إلى ملف pubspec.yaml الخاص بك ضمن قسم assets (مع مراعاة المسافات البادئة الصحيحة):

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^1.0.7
  cupertino_icons: ^1.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true
  assets:
    - assets/index.html

إعداد مكون WebView في واجهة المستخدم

بعد استيراد حزمة webview_flutter إلى ملف main.dart الخاص بنا، حان الوقت لاستبدال عنصر Text الذي أضفناه سابقًا بمكون WebView الفعلي. سنقوم بتغليف WebView ضمن عنصر Scaffold، وهو أمر ضروري لتوفير هيكل أساسي للتطبيق (مثل شريط التطبيق والأزرار العائمة، وسنشرح ذلك لاحقًا).

إليك التعديلات اللازمة على كود _MyHomePageState:

class _MyHomePageState extends State<MyHomePage> {
  WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Webview')),
      body: WebView(
        initialUrl: 'about:blank',
        onWebViewCreated: (WebViewController webviewController) {
          _controller = webviewController;
          _loadHtmlFromAssets();
        },
      ),
    );
  }

  _loadHtmlFromAssets() async {
    String file = await rootBundle.loadString('assets/index.html');
    _controller.loadUrl(
      Uri.dataFromString(file, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString()
    );
  }
}

دعنا نركز على الخصائص المختلفة لمكون WebView التي استخدمناها أعلاه:

  • initialUrl: تحدد هذه الخاصية عنوان URL الأولي الذي سيتم تحميله في WebView. هنا، اخترنا 'about:blank' لأنه لا يوجد URL خارجي محدد، وسنقوم بتحميل ملف HTML المحلي الخاص بنا بدلاً من ذلك.
  • onWebViewCreated: هذه دالة استدعاء (callback) نحصل عليها من الحزمة بمجرد إنشاء WebView. نظرًا لأننا نريد حفظ نسخة من متحكم WebView (WebViewController) الذي نحصل عليه من هذا الاستدعاء، فقد أنشأنا عضوًا خاصًا لتخزينه فيه، وهو _controller.

ستلاحظ أيضًا أننا أنشأنا دالة تسمى _loadHtmlFromAssets، والتي، كما يوحي اسمها، ستقوم بتحميل ملف HTML المحلي الخاص بنا إلى WebView. داخل هذه الدالة، نستخدم نسخة WebViewController الخاصة بنا، _controller، ودالتها المكشوفة loadUrl لتحميل ملف HTML المحلي. نظرًا للمنطق داخل هذه الدالة، فإن تنفيذها غير متزامن (asynchronous).

عند تشغيل تطبيقنا الآن، سنحصل على النتيجة التالية:
شاشة تطبيق Flutter تعرض WebView مع محتوى HTML المحلي 'Hello World!'

التواصل من Flutter إلى WebView

الآن، لنضف بعض الوظائف التي تسمح لنا باستدعاء دالة fromFlutter التي قمنا بتعريفها في ملف HTML المحلي الخاص بنا. لتحقيق ذلك، سنضيف زر إجراء عائم (Floating Action Button أو FAB) إلى تخطيط واجهتنا، وسنربط دالته onPressed باستدعاء دالة fromFlutter. هذا هو أيضًا السبب وراء استخدام عنصر Scaffold، حيث يسهل علينا إضافة عناصر مثل FAB.

إليك التعديلات على دالة build:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Webview')),
    body: WebView(
      initialUrl: 'about:blank',
      javascriptMode: JavascriptMode.unrestricted,
      onWebViewCreated: (WebViewController webviewController) {
        _controller = webviewController;
        _loadHtmlFromAssets();
      },
    ),
    floatingActionButton: FloatingActionButton(
      child: const Icon(Icons.arrow_upward),
      onPressed: () {
        _controller.evaluateJavascript('fromFlutter("From Flutter")');
      },
    ),
  );
}

لإجراء استدعاءات من Flutter إلى محتوى HTML المحمل، نستخدم دالة evaluateJavascript. ولكن لكي نتمكن من استخدامها، يجب علينا إضافة خاصية أخرى إلى مكون WebView تسمى javascriptMode. في الكود أعلاه، قمنا بتعيينها إلى JavascriptMode.unrestricted. إذا لم نقم بتعيين هذه الخاصية، فلن نتمكن من التواصل بين Flutter و WebView.
تطبيق Flutter يقوم بتغيير نص WebView عبر زر الإجراء العائم

التواصل من WebView إلى Flutter

هل تتذكر عندما ذكرت أننا سنتحدث عن محتويات دالة sendBack؟ حان الوقت لذلك الآن.

هذا هو كود دالة sendBack في ملف HTML:

function sendBack() {
  messageHandler.postMessage("Hello from JS");
}

في دالة sendBack، نستخدم كائنًا يُسمى messageHandler، ودالته المرفقة postMessage. تمامًا مثل إنشاء جسر اتصال في تطبيق أصيل، بمجرد إعداده، فإنك تضيف كائنًا إلى الكائن العام Window في طبقة JavaScript لاستخدامه في التواصل. يمكنك تسمية هذا الكائن بأي اسم تريده، طالما أنك تشير إليه عند إجراء استدعاءات من JavaScript إلى تطبيقك الأصيل.

قد تتساءل: كيف يتم إضافة هذا الكائن إلى طبقة JavaScript في تطبيقنا؟ يتم ذلك عن طريق إضافة خاصية javascriptChannels إلى مكون WebView الخاص بنا:

class _MyHomePageState extends State<MyHomePage> {
  WebViewController _controller;
  final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(title: Text('Webview')),
      body: WebView(
        initialUrl: 'about:blank',
        javascriptMode: JavascriptMode.unrestricted,
        javascriptChannels: Set.from([
          JavascriptChannel(
            name: 'messageHandler',
            onMessageReceived: (JavascriptMessage message) {
              _scaffoldKey.currentState.showSnackBar(
                SnackBar(
                  content: Text(message.message)
                )
              );
            })
        ]),
        onWebViewCreated: (WebViewController webviewController) {
          _controller = webviewController;
          _loadHtmlFromAssets();
        },
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.evaluateJavascript('fromFlutter("From Flutter")');
        },
      ),
    );
  }
}

لقد قمنا بتعريف JavascriptChannel باسم (name) ومعالج رسائل (onMessageReceived). الاسم الذي أعطيناه لهذه القناة، messageHandler، هو نفس الاسم الذي نستخدمه للتواصل من ملف HTML المحلي الذي حملناه إلى طبقتنا الأصيلة في Flutter.
تطبيق Flutter يتلقى رسالة من WebView ويعرضها في SnackBar

ملاحظة هامة: للمستخدمين ذوي الملاحظة الدقيقة، ربما لاحظتم إضافة متغير خاص جديد، _scaffoldKey. هذا ضروري لأننا احتجنا إلى إضافة مفتاح (key) إلى عنصر Scaffold الخاص بنا حتى نتمكن من عرض شريط التنبيهات (SnackBar) بشكل صحيح من أي مكان في شجرة الـ widget.

نقاط إضافية وموارد مفيدة

يمكنك الحصول على الكود المصدري الكامل للتطبيق الموضح في هذا المقال من خلال الرابط التالي على GitHub:

https://github.com/TomerPacific/MediumArticles/tree/master/flutter_communication_bridge

هناك نقطتان أخريان يجب الانتباه إليهما عند العمل مع حزمة webview_flutter:

  • دالة alert: حاليًا، دالة alert في JavaScript لا تعمل بشكل صحيح ضمن حزمة webview_flutter.
  • دعم iOS: لاستخدام الحزمة في بيئة iOS، يجب عليك إضافة المفتاح التالي إلى ملف info.plist الخاص بك:
    <key>io.flutter.embedded_views_preview</key><string>yes</string>

فيما يلي بعض المصادر الأخرى التي قد تجدها مفيدة إذا كنت ترغب في تعلم المزيد عن Flutter و WebViews:

الخلاصة التقنية

لقد استعرضنا في هذا المقال دليلًا شاملًا لبناء جسر اتصال ثنائي الاتجاه بين تطبيقات Flutter ومكونات WebView المدمجة. على الرغم من أن Flutter لا يوفر دعمًا أصيلًا لمكونات WebView بشكل مباشر، فقد أظهرنا كيف يمكن لحزمة webview_flutter أن تسد هذه الفجوة بفعالية. تعلمنا كيفية تهيئة WebView، تحميل محتوى HTML محليًا، والأهم من ذلك، كيفية إرسال البيانات من Flutter إلى WebView باستخدام evaluateJavascript، وكيفية استقبال الرسائل من WebView إلى Flutter عبر JavascriptChannels.

هذه القدرة على التواصل المتبادل تفتح آفاقًا واسعة للمطورين لدمج تجارب الويب الغنية والتفاعلية ضمن تطبيقاتهم الأصيلة، مما يتيح مرونة كبيرة في عرض المحتوى الديناميكي وتنفيذ وظائف معقدة قد يكون تطويرها أصيلًا أكثر استهلاكًا للوقت والموارد. إن فهم هذه الآلية يعد حجر الزاوية في بناء تطبيقات هجينة قوية ومرنة تستفيد من أفضل ما في العالمين: سرعة وجمالية Flutter، ومرونة وقوة الويب.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *