إضافة WebView إلى تطبيق Flutter

1. مقدمة

تاريخ آخر تعديل: 19/10/2021

باستخدام مكوّن WebView Flutter الإضافي، يمكنك إضافة تطبيق WebView المصغّر إلى تطبيق Flutter لنظام التشغيل Android أو iOS. على نظام التشغيل iOS، يكون تطبيق WebView مدعومًا بـ WKWebView، في حين يتم دعم تطبيق WebView على نظام Android بواسطة WebView. يمكن أن يعرض المكوّن الإضافي تطبيقات Flutter المصغّرة على عرض الويب. لذلك من الممكن على سبيل المثال عرض قائمة منسدلة على طريقة عرض الويب.

ما الذي ستقوم ببنائه

في هذا الدرس التطبيقي حول الترميز، ستنشئ تطبيقًا للأجهزة الجوّالة خطوة بخطوة لإبراز مكوّن WebView باستخدام حزمة تطوير البرامج (SDK) Flutter. سينفّذ تطبيقك ما يلي:

  • عرض محتوى الويب في WebView
  • عرض تطبيقات Flutter المصغّرة مجمّعة على WebView
  • التفاعل مع أحداث تقدّم تحميل الصفحة
  • التحكّم في WebView من خلال WebViewController
  • حظر المواقع الإلكترونية باستخدام NavigationDelegate
  • تقييم تعبيرات JavaScript
  • التعامل مع عمليات معاودة الاتصال من JavaScript باستخدام JavascriptChannels
  • ضبط ملفات تعريف الارتباط أو إزالتها أو إضافتها أو عرضها
  • تحميل ملف HTML وعرضه من مواد العرض أو الملفات أو السلاسل التي تحتوي على HTML

لقطة شاشة لمحاكي iPhone يشغِّل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية

لقطة شاشة لمحاكي Android يشغل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية

المعلومات التي ستطّلع عليها

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية استخدام مكوّن webview_flutter الإضافي بعدة طرق، بما في ذلك:

  • كيفية إعداد المكوّن الإضافي webview_flutter
  • كيفية الاستماع إلى أحداث تقدّم تحميل الصفحة
  • كيفية التحكّم في التنقّل في الصفحات
  • كيفية توجيه جهاز "WebView" للانتقال إلى الخلف وإلى الأمام خلال السجلّ
  • كيفية تقييم JavaScript، بما في ذلك استخدام النتائج التي تم عرضها
  • كيفية تسجيل عمليات الاستدعاء لاستدعاء رمز Dart من JavaScript
  • كيفية إدارة ملفات تعريف الارتباط
  • كيفية تحميل وعرض صفحات HTML من مواد عرض أو ملفات أو سلسلة تحتوي على HTML

المتطلبات

  • الإصدار 4.1 من "استوديو Android" أو إصدار أحدث (لتطوير تطبيقات Android)
  • Xcode 12 أو إصدار أحدث (لتطوير iOS)
  • Flutter SDK
  • أداة تعديل الرموز، مثل Android Studio أو Visual Studio Code أو Emacs

2. إعداد بيئة تطوير Flutter

لإكمال هذا التمرين، تحتاج إلى برنامجَين، وهما Flutter SDK ومحرِّر.

يمكنك تشغيل الدرس التطبيقي حول الترميز باستخدام أي من الأجهزة التالية:

  • جهاز Android أو iOS فعلي متصل بجهاز الكمبيوتر وتم ضبطه على "وضع مطور البرامج".
  • محاكي iOS (يتطلب تثبيت أدوات Xcode).
  • محاكي Android (يتطلب عملية إعداد في "استوديو Android").

3- الخطوات الأولى

بدء استخدام Flutter

تتوفّر طرق متعددة لإنشاء مشروع جديد على Flutter، من خلال كل من Android Studio وVisual Studio Code يوفران الأدوات اللازمة لهذه المهمة. يمكنك إما اتّباع الإجراءات المرتبطة لإنشاء مشروع، أو تنفيذ الأوامر التالية في وحدة طرفية سهلة الاستخدام لسطر الأوامر.

$ flutter create --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Resolving dependencies in `webview_in_flutter`... 
Downloading packages... 
Got dependencies in `webview_in_flutter`.
Wrote 74 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

Your application code is in webview_in_flutter/lib/main.dart.

إضافة مكوّن WebView Flutter الإضافي كعنصر تابع

من السهل إضافة المزيد من الميزات إلى تطبيق Flutter باستخدام حِزم Pub. في هذا الدرس التطبيقي حول الترميز، ستضيف المكوّن الإضافي webview_flutter إلى مشروعك. شغِّل الأوامر التالية في الوحدة الطرفية.

$ cd webview_in_flutter
$ flutter pub add webview_flutter
Resolving dependencies... 
Downloading packages... 
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ plugin_platform_interface 2.1.8
  test_api 0.7.0 (0.7.1 available)
+ webview_flutter 4.7.0
+ webview_flutter_android 3.16.0
+ webview_flutter_platform_interface 2.10.0
+ webview_flutter_wkwebview 3.13.0
Changed 5 dependencies!
5 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

في حال فحص pubspec.yaml، سيظهر لك سطر في قسم الملحقات للمكون الإضافي webview_flutter.

ضبط حزمة minSDK لنظام التشغيل Android

لاستخدام المكوّن الإضافي webview_flutter على Android، عليك ضبط minSDK على 20. عدِّل ملف android/app/build.gradle على النحو التالي:

android/app/build.gradle

android {
    //...

    defaultConfig {
        applicationId = "com.example.webview_in_flutter"
        minSdk = 20                                         // Modify this line
        targetSdk = flutter.targetSdkVersion
        versionCode = flutterVersionCode.toInteger()
        versionName = flutterVersionName
    }

4. إضافة تطبيق WebView المصغّر إلى تطبيق Flutter

في هذه الخطوة، يجب إضافة WebView إلى تطبيقك. وتكون مكوّنات WebView هي طرق عرض مدمجة مع المحتوى مستضافة، ويمكنك بصفتك مطوّر تطبيقات اختيار كيفية استضافة طرق العرض الأصلية هذه في تطبيقك. على أجهزة Android، يمكنك الاختيار بين الشاشات الافتراضية، التي تكون حاليًا الشاشة التلقائية لنظام Android، والإعداد المختلط. ومع ذلك، يستخدم نظام التشغيل iOS دائمًا طريقة التركيب المختلط.

للحصول على مناقشة معمّقة للاختلافات بين "الشاشات الافتراضية" و"الإنشاء المختلَط"، يُرجى الاطّلاع على المستندات حول استضافة طرق عرض Android وiOS الأصلية في تطبيق Flutter باستخدام طرق عرض النظام الأساسي.

عرض Webview على الشاشة

استبدِل محتوى lib/main.dart بما يلي:

lib/main.dart

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

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: WebViewWidget(
        controller: controller,
      ),
    );
  }
}

يؤدي تشغيل هذا على iOS أو Android إلى عرض WebView كنافذة متصفح لا تظهر بالكامل على جهازك، وهو ما يعني أن المتصفح يظهر على جهازك في وضع ملء الشاشة بدون أي شكل من أشكال الحدود أو الهامش. أثناء التمرير، ستلاحظ أجزاءً من الصفحة قد تبدو غريبة بعض الشيء. ويرجع ذلك إلى أنّ لغة JavaScript غير مفعّلة حاليًا، وبالتالي فإنّ عرض flutter.dev يتطلّب JavaScript.

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

يمكنك تشغيل تطبيق Flutter على أجهزة iOS أو Android للاطّلاع على مكوّن Webview الذي يعرض موقع flutter.dev الإلكتروني. ويمكنك بدلاً من ذلك تشغيل التطبيق باستخدام محاكي Android أو محاكي iOS. لا تتردد في استبدال عنوان URL الأولي لـ WebView بموقعك الإلكتروني على سبيل المثال.

$ flutter run

على افتراض تشغيل المحاكي أو المحاكي المناسب، أو توصيل جهاز فعلي، وبعد تجميع التطبيق ونشره على جهازك، يُفترض أن يظهر لك شيء مثل التالي:

لقطة شاشة لمحاكي iPhone يشغِّل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية

لقطة شاشة لمحاكي Android يشغل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية

5- الاستماع إلى أحداث تحميل الصفحات

يوفّر التطبيق المصغّر WebView العديد من أحداث تقدّم تحميل الصفحات التي يمكن لتطبيقك الاستماع إليها. أثناء دورة تحميل الصفحة WebView، هناك ثلاثة أحداث مختلفة لتحميل الصفحة يتم تنشيطها: onPageStarted وonProgress وonPageFinished. في هذه الخطوة، ستقوم بتنفيذ مؤشر تحميل الصفحة. بالإضافة إلى ذلك، سيُظهر هذا الإجراء أنّه يمكنك عرض محتوى Flutter في مساحة المحتوى الخاصة بـ WebView.

إضافة أحداث تحميل الصفحات إلى تطبيقك

أنشئ ملف مصدر جديدًا على lib/src/web_view_stack.dart واملأه بالمحتوى التالي:

lib/src/web_view_stack.dart

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

class WebViewStack extends StatefulWidget {
  const WebViewStack({super.key});

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setNavigationDelegate(NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
      ))
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

انتهى هذا الرمز من استخدام التطبيق المصغّر WebView مع Stack، ما يؤدي إلى تركيب WebView بشكل مشروط مع LinearProgressIndicator عندما تكون النسبة المئوية لتحميل الصفحة أقل من %100. بما أنّ ذلك يشمل حالة البرنامج التي تتغيّر بمرور الوقت، لقد خزّنت هذه الحالة في فئة State مرتبطة بالسمة StatefulWidget.

للاستفادة من تطبيق WebViewStack المصغّر الجديد، عدِّل lib/main.dart كما يلي:

lib/main.dart

import 'package:flutter/material.dart';

import 'src/web_view_stack.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebViewStack(),
    );
  }
}

عند تشغيل التطبيق، واستنادًا إلى حالة الشبكة وما إذا كان المتصفّح قد خزّن الصفحة التي تنتقل إليها مؤقتًا أم لا، سيظهر لك مؤشر تحميل الصفحة متراكبًا فوق منطقة محتوى WebView.

6- استخدام WebViewController

الوصول إلى WebViewController من تطبيق WebView المصغّر

يتيح تطبيق "WebView" المصغّر إمكانية التحكّم الآلي من خلال WebViewController. تصبح وحدة التحكّم هذه متاحة بعد إنشاء تطبيق "WebView" المصغّر من خلال معاودة الاتصال. الطبيعة غير المتزامنة لمدى توفُّر وحدة التحكّم هذه تجعلها مرشحة رئيسيًا لفئة Completer<T> غير المتزامنة في Dart.

عدِّل "lib/src/web_view_stack.dart" على النحو التالي:

lib/src/web_view_stack.dart

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

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key}); // MODIFY

  final WebViewController controller;                        // ADD

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;
  // REMOVE the controller that was here

  @override
  void initState() {
    super.initState();
    // Modify from here...
    widget.controller.setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
      ),
    );
    // ...to here.
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,                     // MODIFY
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

يستخدم التطبيق المصغّر "WebViewStack" الآن وحدة تحكّم تم إنشاؤها في التطبيق المصغّر المحيط. سيتيح ذلك مشاركة وحدة التحكّم في "WebViewWidget" مع الأجزاء الأخرى من التطبيق بسهولة.

صياغة عناصر التحكّم في التنقّل

وعلى الرغم من أنّ استخدام WebView عامل مهم، إلا أنّ القدرة على الانتقال للأمام والخلف عبر سجلّ الصفحة وإعادة تحميل الصفحة ستكون مفيدة. ولكن لحسن الحظ، يمكنك إضافة هذه الوظيفة إلى تطبيقك باستخدام WebViewController.

أنشئ ملف مصدر جديدًا على lib/src/navigation_controls.dart واملأه بما يلي:

lib/src/navigation_controls.dart

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

class NavigationControls extends StatelessWidget {
  const NavigationControls({required this.controller, super.key});

  final WebViewController controller;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        IconButton(
          icon: const Icon(Icons.arrow_back_ios),
          onPressed: () async {
            final messenger = ScaffoldMessenger.of(context);
            if (await controller.canGoBack()) {
              await controller.goBack();
            } else {
              messenger.showSnackBar(
                const SnackBar(content: Text('No back history item')),
              );
              return;
            }
          },
        ),
        IconButton(
          icon: const Icon(Icons.arrow_forward_ios),
          onPressed: () async {
            final messenger = ScaffoldMessenger.of(context);
            if (await controller.canGoForward()) {
              await controller.goForward();
            } else {
              messenger.showSnackBar(
                const SnackBar(content: Text('No forward history item')),
              );
              return;
            }
          },
        ),
        IconButton(
          icon: const Icon(Icons.replay),
          onPressed: () {
            controller.reload();
          },
        ),
      ],
    );
  }
}

تستخدم هذه الأداة WebViewController التي تمت مشاركتها معها في وقت الإنشاء لتمكين المستخدم من التحكّم في WebView من خلال سلسلة من IconButtons.

إضافة عناصر التحكّم في التنقّل إلى AppBar

بعد توفّر الإصدار المُحدَّث من "WebViewStack" و"NavigationControls" المصمم حديثًا، حان الوقت الآن لتجميع كل ما هو جديد في WebViewApp الجديد. وهذا هو المكان الذي نُنشئ فيه WebViewController المشترك. مع اقتراب العلامة WebViewApp من أعلى شجرة التطبيقات المصغّرة في هذا التطبيق، من المنطقي إنشاءها على هذا المستوى.

عدِّل ملف lib/main.dart على النحو التالي:

lib/main.dart

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

import 'src/navigation_controls.dart';                  // ADD
import 'src/web_view_stack.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  // Add from here...
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }
  // ...to here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        // Add from here...
        actions: [
          NavigationControls(controller: controller),
        ],
        // ...to here.
      ),
      body: WebViewStack(controller: controller),       // MODIFY
    );
  }
}

من المفترض أن يؤدي تشغيل التطبيق إلى إظهار صفحة ويب تتضمن عناصر التحكم:

لقطة شاشة لمحاكي iPhone يشغِّل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية مع عناصر التحكّم في إعادة تحميل الصفحة السابقة والصفحة التالية.

لقطة شاشة لمحاكي Android يشغِّل تطبيق Flutter، مع WebView مضمَّنة تعرض صفحة Flutter.dev الرئيسية مع عناصر التحكُّم في إعادة تحميل الصفحة والصفحة السابقة والصفحة التالية

7. تتبع التنقل باستخدام ميزة Navigationديف

يوفّر WebView لتطبيقك NavigationDelegate, الذي يتيح لتطبيقك تتبُّع مسار التنقّل في صفحة تطبيق WebView المصغّر والتحكّم فيه. عند بدء عملية تنقُّل من خلال WebView,، مثلاً عندما ينقر المستخدم على رابط، يتم استدعاء NavigationDelegate. يمكن استخدام معاودة الاتصال NavigationDelegate للتحكّم في ما إذا كان WebView سيتابع عملية التنقّل.

تسجيل تفويض التنقل المخصص

في هذه الخطوة، سيتم تسجيل طلب معاودة الاتصال NavigationDelegate لحظر التنقّل إلى YouTube.com. يُرجى العِلم أنّ هذا التنفيذ المبسّط يحظر أيضًا محتوى YouTube المضمّن، والذي يظهر في صفحات مستندات واجهة برمجة التطبيقات Flutter API.

عدِّل lib/src/web_view_stack.dart على النحو التالي:

lib/src/web_view_stack.dart

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

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  void initState() {
    super.initState();
    widget.controller.setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
        // Add from here...
        onNavigationRequest: (navigation) {
          final host = Uri.parse(navigation.url).host;
          if (host.contains('youtube.com')) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(
                  'Blocking navigation to $host',
                ),
              ),
            );
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },
        // ...to here.
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

في الخطوة التالية، عليك إضافة عنصر في القائمة لتفعيل اختبار NavigationDelegate باستخدام الفئة WebViewController. ويتم تركها تمرين للقارئ لتعزيز منطق معاودة الاتصال لحظر التنقل بالكامل في الصفحة إلى YouTube.com، مع الاستمرار في السماح بمحتوى YouTube المضمّن في مستندات واجهة برمجة التطبيقات.

8. إضافة زر قائمة إلى AppBar

في الخطوات القليلة التالية، ما عليك سوى إنشاء زر قائمة في تطبيق "AppBar" المصغّر المستخدَم لتقييم JavaScript واستدعاء قنوات JavaScript وإدارة ملفات تعريف الارتباط. بشكل عام، قائمة مفيدة.

أنشئ ملف مصدر جديدًا على lib/src/menu.dart واملأه بما يلي:

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
}

class Menu extends StatelessWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await controller.loadRequest(Uri.parse('https://youtube.com'));
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
      ],
    );
  }
}

عندما يحدد المستخدم خيار القائمة الانتقال إلى YouTube، يتم تنفيذ طريقة WebViewController loadRequest. سيتم حظر التنقّل هذا من خلال معاودة الاتصال "navigationDelegate" التي أنشأتها في الخطوة السابقة.

لإضافة القائمة إلى شاشة WebViewApp، عدِّل lib/main.dart على النحو التالي:

lib/main.dart

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

import 'src/menu.dart';                               // ADD
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..loadRequest(
        Uri.parse('https://flutter.dev'),
      );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
        actions: [
          NavigationControls(controller: controller),
          Menu(controller: controller),               // ADD
        ],
      ),
      body: WebViewStack(controller: controller),
    );
  }
}

شغِّل التطبيق وانقر على عنصر القائمة الانتقال إلى YouTube. من المفترض أن تظهر رسالة SnackBar لإعلامك بأنّ وحدة التحكّم في التنقُّل حظرت الانتقال إلى YouTube.

لقطة شاشة لمحاكي Android يشغِّل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية مع عنصر قائمة يعرض خيار &quot;الانتقال إلى YouTube&quot;

لقطة شاشة لمحاكي Android يشغل تطبيق Flutter، مع WebView مضمَّنة فيه صفحة Flutter.dev الرئيسية مع نافذة منبثقة منبثق بعنوان &quot;حظر التنقّل إلى m.youtube.com&quot;

9. تقييم JavaScript

بإمكان WebViewController تقييم تعبيرات JavaScript في سياق الصفحة الحالية. هناك طريقتان مختلفتان لتقييم JavaScript: لاستخدام رمز JavaScript الذي لا يعرض قيمة، واستخدام runJavaScript، وبالنسبة إلى رمز JavaScript الذي يعرض قيمة، استخدِم runJavaScriptReturningResult.

لتفعيل JavaScript، يجب ضبط WebViewController مع ضبط السمة javaScriptMode على JavascriptMode.unrestricted. بشكل تلقائي، يتم ضبط javascriptMode على JavascriptMode.disabled.

عدِّل فئة _WebViewStackState من خلال إضافة الإعداد javascriptMode على النحو التالي:

lib/src/web_view_stack.dart

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  void initState() {
    super.initState();
    widget.controller
      ..setNavigationDelegate(              // Modify this line to use .. instead of .
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          onNavigationRequest: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..setJavaScriptMode(JavaScriptMode.unrestricted);        // Add this line
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

الآن بعد أن تمكّن WebViewWidget من تنفيذ JavaScript، يمكنك إضافة خيار إلى القائمة لاستخدام طريقة runJavaScriptReturningResult.

باستخدام أداة التحرير أو بعض أعمال لوحة المفاتيح، يمكنك تحويل فئة القائمة إلى StatefulWidget. تعديل lib/src/menu.dart ليتوافق مع ما يلي:

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
  userAgent,                                              // Add this line
}

class Menu extends StatefulWidget {                       // Convert to StatefulWidget
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override                                               // Add from here
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {                    // To here.
  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:           // Modify from here
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));                                           // To here.
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(                // Add from here
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),                                                // To here.
      ],
    );
  }
}

عند النقر على "إظهار وكيل المستخدم" قائمة الخيارات، يتم عرض نتيجة تنفيذ تعبير JavaScript navigator.userAgent في Snackbar. عند تشغيل التطبيق، قد تلاحظ أن صفحة Flutter.dev تبدو مختلفة. هذه هي نتيجة التشغيل مع تفعيل JavaScript.

لقطة شاشة لمحاكي iPhone يشغِّل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية مع عناصر قائمة تعرض خيارات &quot;الانتقال إلى YouTube&quot; أو &quot;إظهار وكيل المستخدم&quot;

لقطة شاشة لمحاكي iPhone يشغِّل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية مع نافذة منبثقة منبثق تعرض سلسلة وكيل المستخدم

10. العمل مع قنوات JavaScript

يتيح تطبيق "قنوات JavaScript" لتطبيقك تسجيل معالِجات معاودة الاتصال في سياق JavaScript الخاص بـ WebViewWidget والتي يمكن استدعاؤها لنقل القيم مرة أخرى إلى رمز Dart الخاص بالتطبيق. في هذه الخطوة، ستسجِّل قناة SnackBar سيتم الاتصال بها على أساس XMLHttpRequest.

عدِّل فئة WebViewStack على النحو التالي:

lib/src/web_view_stack.dart

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<WebViewStack> createState() => _WebViewStackState();
}

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;

  @override
  void initState() {
    super.initState();
    widget.controller
      ..setNavigationDelegate(
        NavigationDelegate(
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          onNavigationRequest: (navigation) {
            final host = Uri.parse(navigation.url).host;
            if (host.contains('youtube.com')) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  content: Text(
                    'Blocking navigation to $host',
                  ),
                ),
              );
              return NavigationDecision.prevent;
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      // Modify from here...
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      );
      // ...to here.
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebViewWidget(
          controller: widget.controller,
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

لكل قناة JavaScript في Set، يتم توفير عنصر قناة في سياق JavaScript على أنّه سمة نافذة تحمل الاسم نفسه لقناة JavaScript name. يتضمّن استخدام هذا من سياق JavaScript استدعاء postMessage على قناة JavaScript لإرسال رسالة يتم تمريرها إلى معالج معاودة الاتصال onMessageReceived المسمى JavascriptChannel المسمى.

للاستفادة من قناة JavaScript التي تمت إضافتها أعلاه، أضِف عنصر قائمة آخر ينفّذ XMLHttpRequest في سياق JavaScript ويُعيد النتائج باستخدام قناة JavaScript SnackBar.

والآن بعد أن تعرّفت WebViewWidget على قنوات JavaScript,، ستتم إضافة مثال لتوسيع نطاق التطبيق. لإجراء ذلك، يجب إضافة المزيد من PopupMenuItem إلى الصف Menu وإضافة الوظائف الإضافية.

عدِّل _MenuOptions باستخدام خيار القائمة الإضافية من خلال إضافة قيمة التعداد javascriptChannel، وأضِف عملية تنفيذ إلى الفئة Menu على النحو التالي:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,                                      // Add this option
}

class Menu extends StatefulWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          case _MenuOptions.javascriptChannel:            // Add from here
            await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');                                          // To here.
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
        const PopupMenuItem<_MenuOptions>(                // Add from here
          value: _MenuOptions.javascriptChannel,
          child: Text('Lookup IP Address'),
        ),                                                // To here.
      ],
    );
  }
}

يتم تنفيذ JavaScript هذا عندما يختار المستخدم خيار القائمة مثال على قناة JavaScript.

var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    SnackBar.postMessage(req.responseText);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();

يرسل هذا الرمز طلب GET إلى واجهة برمجة تطبيقات لعنوان IP العلني، مع عرض عنوان IP للجهاز. تظهر هذه النتيجة في SnackBar من خلال استدعاء postMessage على SnackBar JavascriptChannel.

11. إدارة ملفات تعريف الارتباط

يمكن لتطبيقك إدارة ملفات تعريف الارتباط في WebView باستخدام الفئة CookieManager. في هذه الخطوة، ستظهر قائمة بملفات تعريف الارتباط، وتمحو قائمة ملفات تعريف الارتباط، وتحذف ملفات تعريف الارتباط، وتعيِّن ملفات تعريف ارتباط جديدة. أضِف إدخالات إلى _MenuOptions لكل حالة من حالات استخدام ملفات تعريف الارتباط على النحو التالي:

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  // Add from here ...
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // ... to here.
}

ستركّز بقية التغييرات في هذه الخطوة على الفئة Menu، بما في ذلك تحويل الفئة Menu من فئة "عديمة الحالة" إلى حالة "تسجيل هوية المستخدم". هذا التغيير مهمّ لأنّ Menu يجب أن يمتلك CookieManager، وتُعدّ الحالة القابلة للتغيّر في التطبيقات المصغّرة التي لا تتضمّن حالة تركيبة سيئة.

أضِف CookieManager إلى فئة الحالة الناتجة على النحو التالي:

lib/src/menu.dart

class Menu extends StatefulWidget {
  const Menu({required this.controller, super.key});

  final WebViewController controller;

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> {
  final cookieManager = WebViewCookieManager();       // Add this line

  @override
  Widget build(BuildContext context) {
  // ...

ستحتوي الفئة _MenuState على الرمز الذي تمّت إضافته سابقًا في الفئة Menu، إلى جانب CookieManager التي تمّت إضافتها مؤخرًا. في سلسلة الأقسام التالية، ستضيف دوالّ مساعدة إلى _MenuState والتي سيتم استدعاؤها بدورها من خلال عناصر القائمة التي لم تتم إضافتها بعد.

الحصول على قائمة بجميع ملفات تعريف الارتباط

ستستخدم JavaScript للحصول على قائمة بجميع ملفات تعريف الارتباط. لتحقيق ذلك، أضِف طريقة مساعدة إلى نهاية فئة _MenuState، تُسمى _onListCookies. باستخدام طريقة runJavaScriptReturningResult، تنفّذ الطريقة المساعدة document.cookie في سياق JavaScript، وتعرض قائمة بجميع ملفات تعريف الارتباط.

أضِف ما يلي إلى الفئة _MenuState:

lib/src/menu.dart

Future<void> _onListCookies(WebViewController controller) async {
  final String cookies = await controller
      .runJavaScriptReturningResult('document.cookie') as String;
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
    ),
  );
}

محو جميع ملفات تعريف الارتباط

لمحو جميع ملفات تعريف الارتباط في WebView، استخدِم طريقة clearCookies للفئة CookieManager. تعرض الطريقة علامة Future<bool> تتم مطابقتها إلى true إذا محا CookieManager ملفات تعريف الارتباط، وfalse إذا لم تكن هناك ملفات تعريف ارتباط لمحوها.

أضِف ما يلي إلى الفئة _MenuState:

lib/src/menu.dart

Future<void> _onClearCookies() async {
  final hadCookies = await cookieManager.clearCookies();
  String message = 'There were cookies. Now, they are gone!';
  if (!hadCookies) {
    message = 'There were no cookies to clear.';
  }
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

يمكن إضافة ملف تعريف ارتباط من خلال استدعاء JavaScript. إنّ واجهة برمجة التطبيقات المستخدَمة لإضافة ملف تعريف ارتباط إلى مستند JavaScript موثَّقة بالتفصيل في MDN.

أضِف ما يلي إلى الفئة _MenuState:

lib/src/menu.dart

Future<void> _onAddCookie(WebViewController controller) async {
  await controller.runJavaScript('''var date = new Date();
  date.setTime(date.getTime()+(30*24*60*60*1000));
  document.cookie = "FirstName=John; expires=" + date.toGMTString();''');
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

يمكن أيضًا ضبط ملفات تعريف الارتباط باستخدام CookieManager على النحو التالي.

أضِف ما يلي إلى الفئة _MenuState:

lib/src/menu.dart

Future<void> _onSetCookie(WebViewController controller) async {
  await cookieManager.setCookie(
    const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
  );
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie is set.'),
    ),
  );
}

تتضمّن إزالة ملف تعريف ارتباط إضافة ملف تعريف ارتباط، يكون تاريخ انتهاء صلاحيته محدَّدًا في الماضي.

أضِف ما يلي إلى الفئة _MenuState:

lib/src/menu.dart

Future<void> _onRemoveCookie(WebViewController controller) async {
  await controller.runJavaScript(
      'document.cookie="FirstName=John; expires=Thu, 01 Jan 1970 00:00:00 UTC" ');
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie removed.'),
    ),
  );
}

إضافة عناصر قائمة CookieManager

كل ما يتبقى هو إضافة خيارات القائمة، وربطها بالطرق المساعدة التي أضفتها للتو. عدِّل فئة _MenuState على النحو التالي:

lib/src/menu.dart

class _MenuState extends State<Menu> {
  final cookieManager = WebViewCookieManager();

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          case _MenuOptions.javascriptChannel:
            await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
          case _MenuOptions.clearCookies:                        // Add from here
            await _onClearCookies();
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);            // To here.
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.javascriptChannel,
          child: Text('Lookup IP Address'),
        ),
        const PopupMenuItem<_MenuOptions>(                       // Add from here
          value: _MenuOptions.clearCookies,
          child: Text('Clear cookies'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.listCookies,
          child: Text('List cookies'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.addCookie,
          child: Text('Add cookie'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.setCookie,
          child: Text('Set cookie'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.removeCookie,
          child: Text('Remove cookie'),
        ),                                                       // To here.
      ],
    );
  }

ممارسة "مدير ملفات تعريف الارتباط"

لاستخدام جميع الوظائف التي أضفتها للتو إلى التطبيق، جرِّب الخطوات التالية:

  1. اختَر إدراج ملفات تعريف الارتباط. يجب أن يسرد هذا العنصر ملفات تعريف الارتباط في "إحصاءات Google" التي حدّدها flutter.dev.
  2. انقر على محو ملفات تعريف الارتباط. من المفترض أن يتم الإبلاغ عن أن ملفات تعريف الارتباط قد تم محوها بالفعل.
  3. اختَر محو ملفات تعريف الارتباط مرة أخرى. من المفترض أن يتم الإبلاغ عن عدم توفر ملفات تعريف ارتباط لمحوها.
  4. اختَر إدراج ملفات تعريف الارتباط. يجب أن يبلغك بعدم وجود ملفات تعريف ارتباط.
  5. انقر على إضافة ملف تعريف الارتباط. من المفترض أن يُبلغ عن ملف تعريف الارتباط كمضاف.
  6. اختَر ضبط ملف تعريف الارتباط. من المفترض أن يُبلغ عن ملف تعريف الارتباط على أنّه تم ضبطه.
  7. اختَر إدراج ملفات تعريف الارتباط، ثم اختَر إزالة ملف تعريف الارتباط كعنصر نهائي.

لقطة شاشة لمحاكي Android يشغِّل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض صفحة Flutter.dev الرئيسية مع قائمة من خيارات القائمة التي تغطي الانتقال إلى YouTube وعرض وكيل المستخدم والتفاعل مع حاوية ملفات تعريف الارتباط في المتصفّح

لقطة شاشة لمحاكي Android يشغِّل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض الصفحة الرئيسية لموقع Flutter.dev الرئيسية مع نافذة منبثقة تعرض معلومات ملفات تعريف الارتباط التي تم ضبطها في المتصفِّح

لقطة شاشة لمحاكي Android يعمل على تشغيل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض الصفحة الرئيسية لموقع Flutter.dev الرئيسية مع نافذة منبثقة تعرض الرسالة &quot;كانت هناك ملفات تعريف ارتباط&quot;. وها هي الآن قد اختفت!&quot;

لقطة شاشة لمحاكي Android يشغِّل تطبيق Flutter، مع مكوّن WebView مضمَّن يعرض الصفحة الرئيسية لموقع Flutter.dev الرئيسية مع نافذة منبثقة تعرض الرسالة &quot;تمّت إضافة ملف تعريف ارتباط مخصّص&quot;.

12. تحميل مواد العرض والملفات وسلاسل HTML في Flutter في WebView

يمكن لتطبيقك تحميل ملفات HTML باستخدام طرق مختلفة وعرضها في WebView. في هذه الخطوة، سيتم تحميل مادة عرض في Flutter محدَّدة في ملف pubspec.yaml، وتحميل ملف في المسار المحدَّد وتحميل صفحة باستخدام سلسلة HTML.

إذا أردت تحميل ملف يقع في مسار محدّد، عليك إضافة السمة path_provider إلى pubspec.yaml. هذا هو مكوّن Flutter الإضافي للعثور على المواقع الجغرافية الشائعة الاستخدام على نظام الملفات.

في سطر الأوامر، شغِّل الأمر التالي:

$ flutter pub add path_provider

لتحميل مادة العرض، علينا تحديد المسار الذي يؤدي إلى مادة العرض في pubspec.yaml. في pubspec.yaml، أضِف السطور التالية:

pubspec.yaml

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  # Add from here
  assets:
    - assets/www/index.html
    - assets/www/styles/style.css
  # ... to here.

لإضافة مواد العرض إلى مشروعك، اتّبِع الخطوات التالية:

  1. أنشِئ دليلاً جديدًا باسم "assets" في المجلد الجذر لمشروعك.
  2. أنشِئ دليلاً جديدًا باسم "www" في مجلد "assets".
  3. أنشِئ دليلاً جديدًا باسم "styles" في مجلد "www".
  4. أنشئ ملفًا جديدًا بالاسم index.html في المجلد www.
  5. أنشئ ملفًا جديدًا بالاسم style.css في المجلد styles.

انسخ الرمز التالي والصقه في ملف index.html:

assets/www/index.html

<!DOCTYPE html>
<!-- Copyright 2013 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<html lang="en">
<head>
    <title>Load file or HTML string example</title>
    <link rel="stylesheet" href="styles/style.css" />
</head>
<body>

<h1>Local demo page</h1>
<p>
    This is an example page used to demonstrate how to load a local file or HTML
    string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
    webview</a> plugin.
</p>

</body>
</html>

بالنسبة إلى style.css، استخدم الأسطر القليلة التالية لتعيين نمط رأس HTML:

assets/www/styles/style.css

h1 {
  color: blue;
}

بعد أن تم ضبط مواد العرض وأصبحت جاهزة للاستخدام، يمكنك تنفيذ الطرق اللازمة لتحميل مواد العرض أو الملفات أو سلاسل HTML في Flutter وعرضها.

تحميل مادة عرض Flutter

لتحميل مادة العرض التي أنشأتها للتو، ما عليك سوى استدعاء طريقة loadFlutterAsset باستخدام WebViewController وتحديد المسار إلى مادة العرض كمَعلمة. أضف الطريقة التالية في نهاية التعليمة البرمجية:

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
    WebViewController controller, BuildContext context) async {
  await controller.loadFlutterAsset('assets/www/index.html');
}

تحميل ملف على الجهاز

لتحميل ملف على جهازك، يمكنك إضافة طريقة تستخدم طريقة loadFile، ومرة أخرى باستخدام WebViewController التي تأخذ String التي تحتوي على المسار إلى الملف.

تحتاج أولاً إلى إنشاء ملف يحتوي على رمز HTML. ويمكنك إجراء ذلك ببساطة من خلال إضافة رمز HTML كسلسلة أعلى الرمز في ملف menu.dart أسفل عمليات الاستيراد مباشرةً.

lib/src/menu.dart

import 'dart:io';                                   // Add this line,
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';  // And this one.
import 'package:webview_flutter/webview_flutter.dart';

// Add from here ...
const String kExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>

<h1>Local demo page</h1>
<p>
 This is an example page used to demonstrate how to load a local file or HTML
 string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
 webview</a> plugin.
</p>

</body>
</html>
''';
// ... to here.

لإنشاء File وكتابة سلسلة HTML إلى الملف، عليك إضافة طريقتَين. ستحمِّل _onLoadLocalFileExample الملف من خلال توفير المسار كسلسلة يتم عرضها باستخدام طريقة _prepareLocalFile(). أضف الطرق التالية إلى التعليمات البرمجية:

lib/src/menu.dart

Future<void> _onLoadLocalFileExample(
    WebViewController controller, BuildContext context) async {
  final String pathToIndex = await _prepareLocalFile();

  await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
  final String tmpDir = (await getTemporaryDirectory()).path;
  final File indexFile = File('$tmpDir/www/index.html');

  await Directory('$tmpDir/www').create(recursive: true);
  await indexFile.writeAsString(kExamplePage);

  return indexFile.path;
}

تحميل سلسلة HTML

يعد عرض صفحة عن طريق توفير سلسلة HTML أمرًا مباشرًا إلى حد ما. تتضمن WebViewController طريقة يمكنك استخدامها تُسمى loadHtmlString حيث يمكنك منح سلسلة HTML كوسيطة. بعد ذلك، سيعرض WebView صفحة HTML المقدّمة. أضف الطريقة التالية إلى التعليمة البرمجية:

lib/src/menu.dart

Future<void> _onLoadFlutterAssetExample(
    WebViewController controller, BuildContext context) async {
  await controller.loadFlutterAsset('assets/www/index.html');
}

Future<void> _onLoadLocalFileExample(
    WebViewController controller, BuildContext context) async {
  final String pathToIndex = await _prepareLocalFile();

  await controller.loadFile(pathToIndex);
}

static Future<String> _prepareLocalFile() async {
  final String tmpDir = (await getTemporaryDirectory()).path;
  final File indexFile = File('$tmpDir/www/index.html');

  await Directory('$tmpDir/www').create(recursive: true);
  await indexFile.writeAsString(kExamplePage);

  return indexFile.path;
}

// Add here ...
Future<void> _onLoadHtmlStringExample(
    WebViewController controller, BuildContext context) async {
  await controller.loadHtmlString(kExamplePage);
}
// ... to here.

إضافة الأصناف في القائمة

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

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
  listCookies,
  clearCookies,
  addCookie,
  setCookie,
  removeCookie,
  // Add from here ...
  loadFlutterAsset,
  loadLocalFile,
  loadHtmlString,
  // ... to here.
}

الآن بعد أن تم تحديث التعداد، يمكنك إضافة خيارات القائمة وربطها بالطرق المساعدة التي أضفتها للتو. عدِّل فئة _MenuState على النحو التالي:

lib/src/menu.dart

class _MenuState extends State<Menu> {
  final cookieManager = WebViewCookieManager();

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<_MenuOptions>(
      onSelected: (value) async {
        switch (value) {
          case _MenuOptions.navigationDelegate:
            await widget.controller
                .loadRequest(Uri.parse('https://youtube.com'));
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!context.mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
          case _MenuOptions.javascriptChannel:
            await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
  if (req.status == 200) {
    let response = JSON.parse(req.responseText);
    SnackBar.postMessage("IP Address: " + response.ip);
  } else {
    SnackBar.postMessage("Error: " + req.status);
  }
}
req.send();''');
          case _MenuOptions.clearCookies:
            await _onClearCookies();
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
          case _MenuOptions.loadFlutterAsset:             // Add from here
            if (!mounted) return;
            await _onLoadFlutterAssetExample(widget.controller, context);
          case _MenuOptions.loadLocalFile:
            if (!mounted) return;
            await _onLoadLocalFileExample(widget.controller, context);
          case _MenuOptions.loadHtmlString:
            if (!mounted) return;
            await _onLoadHtmlStringExample(widget.controller, context);
                                                          // To here.
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.javascriptChannel,
          child: Text('Lookup IP Address'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.clearCookies,
          child: Text('Clear cookies'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.listCookies,
          child: Text('List cookies'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.addCookie,
          child: Text('Add cookie'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.setCookie,
          child: Text('Set cookie'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.removeCookie,
          child: Text('Remove cookie'),
        ),
        const PopupMenuItem<_MenuOptions>(                // Add from here
          value: _MenuOptions.loadFlutterAsset,
          child: Text('Load Flutter Asset'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.loadHtmlString,
          child: Text('Load HTML string'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.loadLocalFile,
          child: Text('Load local file'),
        ),                                                // To here.
      ],
    );
  }

اختبار مواد العرض والملف وسلسلة HTML

لاختبار ما إذا كانت التعليمة البرمجية تعمل بعد تنفيذك لها للتو، يمكنك تشغيل الرمز على جهازك والنقر على أحد عناصر القائمة المضافة حديثًا. لاحظ كيف يستخدم _onLoadFlutterAssetExample اللون style.css الذي أضفناه لتغيير عنوان ملف HTML إلى اللون الأزرق.

لقطة شاشة لمحاكي Android يعمل على تشغيل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة باسم &quot;صفحة العرض التوضيحي المحلية&quot; مع العنوان باللون الأزرق

لقطة شاشة لمحاكي Android يعمل على تشغيل تطبيق Flutter مع مكوّن WebView مضمَّن يعرض صفحة باسم &quot;صفحة العرض التوضيحي المحلية&quot; مع العنوان باللون الأسود

13. أكملت كل الإجراءات

تهانينا! لقد أكملت الدرس التطبيقي حول الترميز. يمكنك العثور على الرمز المكتمل لهذا الدرس التطبيقي في مستودع الدرس التطبيقي حول الترميز.

لمزيد من المعلومات، يمكنك تجربة الدروس التطبيقية حول ترميز Flutter.