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

1. مقدمة

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

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

ما ستنشئه

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

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

محاكي iPhone يعرض تطبيق Flutter يتضمّن عرضًا مضمّنًا لصفحة Flutter.dev الرئيسية

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

المُعطيات

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

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

المتطلبات

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

يجب توفّر برنامجَين لإكمال هذا الدرس التطبيقي، وهما حزمة تطوير البرامج (SDK) الخاصة بإطار عمل Flutter ومحرِّر.

يمكنك تشغيل الدرس العملي باستخدام أيّ من الأجهزة التالية:

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

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

بدء استخدام Flutter

تتوفّر طرق متنوعة لإنشاء مشروع Flutter جديد، إذ يوفّر كلّ من "استوديو Android" و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...
  collection 1.18.0 (1.19.0 available)
  leak_tracker 10.0.5 (10.0.7 available)
  leak_tracker_flutter_testing 3.0.5 (3.0.7 available)
  material_color_utilities 0.11.1 (0.12.0 available)
+ plugin_platform_interface 2.1.8
  string_scanner 1.2.0 (1.3.0 available)
  test_api 0.7.2 (0.7.3 available)
+ webview_flutter 4.9.0
+ webview_flutter_android 3.16.7
+ webview_flutter_platform_interface 2.10.0
+ webview_flutter_wkwebview 3.15.0
Changed 5 dependencies!
6 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 إلى تطبيقك. إنّ WebViews هي طرق عرض مضمّنة مستضافة، ويمكنك كمطوّر تطبيقات اختيار طريقة استضافة طرق العرض المضمّنة هذه في تطبيقك. على نظام التشغيل 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 يتضمّن عرضًا مضمّنًا لصفحة Flutter.dev الرئيسية

محاكي Android يشغّل تطبيق Flutter مع عرض مضمّن لصفحة 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 من خلال سلسلة من IconButton.

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

بعد تعديل 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 مع عرض ويب مضمّن يعرض الصفحة الرئيسية لموقع Flutter.dev الإلكتروني مع عناصر التحكّم في الصفحة السابقة والصفحة التالية وإعادة تحميل الصفحة

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

7. تتبُّع التنقّل باستخدام NavigationDelegate

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

تسجيل NavigationDelegate مخصّص

في هذه الخطوة، ستسجّل 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 تُستخدَم لتقييم 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، يتم تنفيذ طريقة loadRequest في WebViewController. سيتم حظر عملية التنقّل هذه من خلال ردّ الاتصال 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 مع عرض ويب مضمّن يعرض الصفحة الرئيسية لموقع Flutter.dev الإلكتروني مع عنصر قائمة يعرض خيار &quot;الانتقال إلى YouTube&quot;

محاكي Android يعرض تطبيق Flutter مع عرض ويب مضمّن يعرض صفحة 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 مع عرض مضمّن لصفحة Flutter.dev الرئيسية في webview مع عناصر قائمة تعرض خيارات &quot;الانتقال إلى YouTube&quot; أو &quot;عرض وكيل المستخدم&quot;

محاكي iPhone يشغّل تطبيق Flutter مع عرض مضمّن لصفحة الويب يعرض الصفحة الرئيسية لموقع 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 موضّحة بالتفصيل على شبكة مطوّري Mozilla.

أضِف ما يلي إلى فئة _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.
      ],
    );
  }

استخدام CookieManager

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

  1. اختَر عرض ملفات تعريف الارتباط. من المفترض أن تعرض ملفات تعريف الارتباط في "إحصاءات Google" التي تم ضبطها من خلال flutter.dev.
  2. انقر على محو ملفات تعريف الارتباط. من المفترض أن يوضّح أنّه تم محو ملفات تعريف الارتباط بالفعل.
  3. انقر على محو ملفات تعريف الارتباط مرة أخرى. يجب أن يُبلغ بأنّه لم تتوفّر أي ملفات تعريف ارتباط لمحو بياناتها.
  4. اختَر عرض ملفات تعريف الارتباط. يجب أن يُظهر أنّه لا تتوفّر أي ملفات تعريف ارتباط.
  5. انقر على إضافة ملف تعريف ارتباط. يجب أن يُبلغ عن إضافة ملف تعريف الارتباط.
  6. انقر على ضبط ملف تعريف الارتباط. يجب أن يُبلغ عن ضبط ملف تعريف الارتباط.
  7. انقر على عرض ملفات تعريف الارتباط (List cookies)، ثم انقر على إزالة ملف تعريف الارتباط (Remove cookie).

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

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

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

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

12. تحميل مواد عرض Flutter وملفاتها وسلاسل HTML في 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 البرمجية الأخرى.