הוספת WebView לאפליקציית Flutter

1. מבוא

העדכון האחרון: 19 באוקטובר 2021

באמצעות התוסף WebView Flutter אפשר להוסיף ווידג'ט WebView לאפליקציית Flutter ל-Android או ל-iOS. ב-iOS, הווידג'ט WebView מגובה על ידי WKWebView, וב-Android, הווידג'ט WebView מגובה על ידי WebView. התוסף יכול להציג ווידג'טים של Flutter בתצוגת האינטרנט. לדוגמה, אפשר להציג תפריט נפתח מעל תצוגת האינטרנט.

מה תפַתחו

בשיעור הזה תלמדו איך ליצור אפליקציה לנייד שלב אחר שלב עם WebView באמצעות Flutter SDK. האפליקציה שלכם:

  • הצגת תוכן אינטרנט בWebView
  • הצגת ווידג'טים של Flutter בערימה מעל WebView
  • תגובה לאירועים של התקדמות טעינת הדף
  • שליטה ב-WebView דרך WebViewController
  • חסימת אתרים באמצעות NavigationDelegate
  • הערכת ביטויי JavaScript
  • טיפול בקריאות חוזרות (callback) מ-JavaScript באמצעות JavascriptChannels
  • הגדרה, הסרה, הוספה או הצגה של קובצי Cookie
  • טעינה והצגה של HTML מנכסים, מקבצים או ממחרוזות שמכילות HTML

סימולטור אייפון שמריץ אפליקציית Flutter עם תצוגת WebView מוטמעת שמציגה את דף הבית של Flutter.dev

אמולטור Android שמריץ אפליקציית Flutter עם WebView מוטמע שמציג את דף הבית של Flutter.dev

מה תלמדו?

ב-Codelab הזה תלמדו איך להשתמש בתוסף webview_flutter במגוון דרכים, כולל:

  • איך מגדירים את הפלאגין webview_flutter
  • איך להאזין לאירועים של התקדמות טעינת הדף
  • איך שולטים בניווט בדף
  • איך מנחים את WebView לנוע קדימה ואחורה בהיסטוריה שלו
  • איך מעריכים JavaScript, כולל שימוש בתוצאות שהוחזרו
  • איך רושמים קריאות חוזרות (callbacks) כדי לקרוא לקוד Dart מ-JavaScript
  • כיצד לנהל קובצי cookie
  • איך לטעון ולהציג דפי HTML מנכסים, מקבצים או ממחרוזת שמכילה HTML

מה צריך?

2. הגדרת סביבת הפיתוח של Flutter

כדי להשלים את שיעור ה-Lab הזה, תצטרכו שני סוגי תוכנה: Flutter SDK ועורך.

אפשר להריץ את ה-codelab באמצעות כל אחד מהמכשירים הבאים:

‫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. ב-codelab הזה תוסיפו את תוסף 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, תראו שיש בו עכשיו שורה בקטע dependencies של הפלאגין 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 מושבת, ונדרשת הפעלה של JavaScript כדי לעבד את flutter.dev בצורה תקינה.

הפעלת האפליקציה

מריצים את האפליקציה ב-Flutter ב-iOS או ב-Android כדי לראות Webview שבו מוצג האתר flutter.dev. אפשר גם להריץ את האפליקציה באמולטור Android או בסימולטור iOS. אתם יכולים להחליף את כתובת ה-URL הראשונית של WebView, למשל בכתובת של האתר שלכם.

$ flutter run

בהנחה שהסימולטור או האמולטור המתאימים פועלים, או שמכשיר פיזי מחובר, אחרי הידור ופריסה של האפליקציה במכשיר, אמור להופיע משהו כמו הדוגמה הבאה:

סימולטור אייפון שמריץ אפליקציית 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 באמצעות סדרה של 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
    );
  }
}

כשמפעילים את האפליקציה, אמור להיפתח דף אינטרנט עם אמצעי בקרה:

סימולטור אייפון שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם אמצעי בקרה של דף קודם, דף הבא וטעינה מחדש של הדף

אמולטור של Android שמריץ אפליקציית Flutter עם תצוגת WebView מוטמעת שמציגה את דף הבית של Flutter.dev עם אמצעי בקרה של דף קודם, דף הבא וטעינה מחדש של הדף

7. מעקב אחרי הניווט באמצעות NavigationDelegate

WebView מספק לאפליקציה שלך NavigationDelegate, שמאפשר לאפליקציה לעקוב אחרי הניווט בדף של הווידג'ט WebView ולשלוט בו. כשמתחילים בניווט באמצעות WebView,, למשל כשמשתמש לוחץ על קישור, מתבצעת קריאה ל-NavigationDelegate. אפשר להשתמש בקריאה החוזרת NavigationDelegate כדי לקבוע אם WebView ימשיך בניווט.

רישום של NavigationDelegate מותאם אישית

בשלב הזה, תרשמו NavigationDelegate callback כדי לחסום את הניווט אל 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 בתוך השורה במסמכי ה-API.

8. הוספת כפתור תפריט לסרגל האפליקציות

בשלבים הבאים ניצור לחצן תפריט בווידג'ט AppBar שמשמש להערכת JavaScript, להפעלת ערוצי JavaScript ולניהול קובצי Cookie. בקיצור, תפריט שימושי מאוד.

יוצרים קובץ מקור חדש ב-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'),
        ),
      ],
    );
  }
}

כשמשתמש בוחר באפשרות בתפריט Navigate to 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),
    );
  }
}

מריצים את האפליקציה ומקישים על האפשרות Navigate to YouTube (ניווט אל YouTube) בתפריט. אמורה להופיע הודעה קצרה שמודיעה שבקר הניווט חסם את הניווט אל YouTube.

אמולטור של Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם פריט בתפריט שמציג את האפשרות &#39;ניווט אל YouTube&#39;

אמולטור Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם הודעה קופצת שכתוב בה &#39;חסימת הניווט אל m.youtube.com&#39;

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.

באמצעות Editor או פעולות במקלדת, ממירים את המחלקה Menu למחלקה 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.
      ],
    );
  }
}

כשמקישים על אפשרות התפריט 'הצגת מחרוזת User-Agent', התוצאה של הפעלת הביטוי JavaScript‏ navigator.userAgent מוצגת ב-Snackbar. כשמריצים את האפליקציה, יכול להיות שתשימו לב שהדף Flutter.dev נראה שונה. זאת התוצאה של ההרצה עם JavaScript מופעל.

סימולטור של אייפון שבו פועלת אפליקציית Flutter עם תצוגת WebView מוטמעת שמציגה את דף הבית של Flutter.dev עם פריטי תפריט שמציגים את האפשרויות &#39;ניווט אל YouTube&#39; או &#39;הצגת סוכן משתמש&#39;

סימולטור של אייפון שבו פועלת אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם חלון קופץ של הודעה קצרה שמופיע בה מחרוזת של סוכן משתמש.

10. עבודה עם ערוצי JavaScript

ערוצי JavaScript מאפשרים לאפליקציה לרשום פונקציות callback לטיפול באירועים בהקשר 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 כדי לשלוח הודעה שמועברת ל-JavascriptChannel בשם של onMessageReceived handler של קריאה חוזרת (callback).

כדי להשתמש בערוץ 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 Channel Example.

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 ל-API של כתובת IP ציבורית, ומחזיר את כתובת ה-IP של המכשיר. התוצאה הזו מוצגת ב-SnackBar על ידי הפעלת postMessage ב-SnackBar JavascriptChannel.

11. ניהול קובצי Cookie

האפליקציה יכולה לנהל קובצי Cookie ב-WebView באמצעות המחלקה CookieManager. בשלב הזה, תציגו רשימה של קובצי Cookie, תנקו את רשימת קובצי ה-Cookie, תמחקו קובצי Cookie ותגדירו קובצי Cookie חדשים. מוסיפים רשומות ל-_MenuOptions לכל אחד מתרחישי השימוש בקובצי Cookie באופן הבא:

lib/src/menu.dart

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

שאר השינויים בשלב הזה מתמקדים במחלקה Menu, כולל ההמרה של המחלקה Menu ממצב חסר מצב למצב עם מצב. השינוי הזה חשוב כי Menu צריך להיות הבעלים של CookieManager, ומצב שניתן לשינוי בווידג'טים חסרי מצב הוא שילוב בעייתי.

מוסיפים את CookieManager למחלקת State שמתקבלת באופן הבא:

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) {
  // ...

הקוד שנוסף קודם בכיתה Menu יופיע בכיתה _MenuState, יחד עם CookieManager שנוסף עכשיו. בסדרת הקטעים הבאה, תוסיפו ל-_MenuState פונקציות עזר שיופעלו על ידי פריטי התפריט שעדיין לא נוספו.

קבלת רשימה של כל קובצי ה-Cookie

תשתמשו ב-JavaScript כדי לקבל רשימה של כל קובצי ה-Cookie. כדי לעשות את זה, מוסיפים שיטת עזר לסוף המחלקה _MenuState, שנקראת _onListCookies. באמצעות ה-method‏ runJavaScriptReturningResult, שיטת העזר שלכם מפעילה את document.cookie בהקשר של JavaScript, ומחזירה רשימה של כל קובצי ה-Cookie.

מוסיפים את הטקסט הבא לכיתה _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.'),
    ),
  );
}

ניקוי כל קובצי ה-Cookie

כדי למחוק את כל קובצי ה-Cookie ב-WebView, משתמשים בשיטה clearCookies של המחלקה CookieManager. השיטה מחזירה Future<bool> שמוביל ל-true אם CookieManager ניקה את קובצי ה-Cookie, ול-false אם לא היו קובצי Cookie לניקוי.

מוסיפים את הטקסט הבא לכיתה _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),
    ),
  );
}

אפשר להוסיף קובץ Cookie באמצעות הפעלה של JavaScript. ממשק ה-API שמשמש להוספת קובץ Cookie למסמך 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.'),
    ),
  );
}

אפשר גם להגדיר קובצי Cookie באמצעות 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.'),
    ),
  );
}

הסרה של קובץ Cookie כוללת הוספה של קובץ Cookie עם תאריך תפוגה שמוגדר בעבר.

מוסיפים את הטקסט הבא לכיתה _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. בוחרים באפשרות List cookies (רשימת קובצי Cookie). אמורים להופיע בו קובצי ה-Cookie של Google Analytics שהוגדרו על ידי flutter.dev.
  2. בוחרים באפשרות מחיקת קובצי Cookie. הוא צריך לדווח שקובצי ה-Cookie אכן נוקו.
  3. בוחרים שוב באפשרות מחיקת קובצי Cookie. הדוח צריך לציין שלא היו קובצי Cookie לניקוי.
  4. בוחרים באפשרות List cookies (רשימת קובצי Cookie). הכלי אמור לדווח שאין קובצי Cookie.
  5. לוחצים על הוספת קובץ Cookie. היא צריכה לדווח על קובץ ה-Cookie כקובץ שנוסף.
  6. בוחרים באפשרות הגדרת קובץ Cookie. הוא אמור לדווח שקובץ ה-Cookie הוגדר.
  7. בוחרים באפשרות List cookies (רשימת קובצי Cookie), ואז בוחרים באפשרות Remove cookie (הסרת קובץ Cookie).

אמולטור Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם רשימה של אפשרויות בתפריט, כולל מעבר ל-YouTube, הצגת סוכן משתמש ואינטראקציה עם מאגר קובצי ה-Cookie של הדפדפן

אמולטור של Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה את דף הבית של Flutter.dev עם תיבת דו-שיח של הודעה קופצת שמציגה את קובצי ה-Cookie שהוגדרו בדפדפן

אמולטור של Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שבה מוצג דף הבית של Flutter.dev עם תיבת דו-שיח קופצת שכתוב בה &#39;יש קובצי Cookie&#39;. עכשיו הם נעלמו!&#39;

אמולטור של Android שמריץ אפליקציית Flutter עם רכיב WebView מוטמע שמציג את דף הבית של Flutter.dev עם תיבת דו-שיח קופצת שכתוב בה &#39;נוסף קובץ Cookie בהתאמה אישית&#39;.

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>

כדי להגדיר את הסגנון של כותרת ה-HTML, משתמשים בשורות הבאות בקובץ style.css:

assets/www/styles/style.css

h1 {
  color: blue;
}

עכשיו, אחרי שהגדרתם את הנכסים והם מוכנים לשימוש, אתם יכולים להטמיע את השיטות שנדרשות לטעינה ולהצגה של נכסי Flutter, קבצים או מחרוזות HTML.

טעינת נכס 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 כארגומנט. הדף ב-HTML שצוין יוצג ב-WebView. מוסיפים את השיטה הבאה לקוד:

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.
}

אחרי שמעדכנים את ה-enum, אפשר להוסיף את האפשרויות בתפריט ולקשר אותן לשיטות העזר שהוספתם. מעדכנים את הכיתה _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 מוטמעת שמציגה דף עם התווית &#39;Local demo page&#39; והכותרת בכחול

אמולטור Android שמריץ אפליקציית Flutter עם תצוגת אינטרנט מוטמעת שמציגה דף עם התווית Local demo page (דף הדגמה מקומי) והכותרת בשחור

13. הכול מוכן!

חדשות טובות!!! סיימתם את ה-Codelab. אפשר למצוא את הקוד המלא של ה-Codelab הזה במאגר ה-Codelab.

כדי לקבל מידע נוסף, אפשר לנסות את הסדנאות האחרות ללימוד Flutter.