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

1. מבוא

עדכון אחרון: 19.10.2021

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

מה תפַתחו

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

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

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

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

מה תלמדו

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

  • איך להגדיר את הפלאגין webview_flutter
  • איך להאזין לאירועי התקדמות של טעינת דף
  • איך לשלוט בניווט בדפים
  • איך להורות על ה-WebView לחזור אחורה וקדימה בהיסטוריה שלו
  • איך לבדוק את JavaScript, כולל שימוש בתוצאות שהוחזרו
  • איך רושמים קריאות חוזרות (callback) לקריאה של קוד Drt מ-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 כתלות

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

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

אם בודקים את ה-pubspec.yaml, רואים עכשיו שיש בו שורה בקטע יחסי התלות של הפלאגין webview_flutter.

הגדרת Android minSDK

כדי להשתמש בפלאגין webview_flutter ב-Android, צריך להגדיר את minSDK לערך 20. משנים את קובץ android/app/build.gradle באופן הבא:

android/app/build.gradle

android {
    //...

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

4. הוספת ווידג'ט של WebView לאפליקציית Flutter

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

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

הצבת WebView במסך

מחליפים את התוכן של lib/main.dart בערך הבא:

lib/main.dart

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

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

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

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

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

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

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

כשמפעילים את האפשרות הזו ב-iOS או ב-Android, WebView מציג כחלון דפדפן עם גלישה מלאה במכשיר. המשמעות היא שהדפדפן מוצג במכשיר במסך מלא ללא שוליים או שוליים. במהלך הגלילה, תבחינו בחלקים בדף שעשויים להיראות קצת מוזרים. הסיבה לכך היא ש-JavaScript מושבת כרגע ועיבוד flutter.dev כראוי דורש JavaScript.

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

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

$ flutter run

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

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

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

5. האזנה לאירועים של טעינת דף

הווידג'ט WebView מספק כמה אירועי התקדמות של טעינת דפים, שהאפליקציה יכולה להאזין להם. במהלך מחזור טעינת הדף WebView, מופעלים שלושה אירועים שונים של טעינת דפים: onPageStarted, onProgress ו-onPageFinished. בשלב הזה תטמיעו אינדיקטור של טעינת דף. כבונוס, נראה שיש לך אפשרות לעבד תוכן ב-Flutter באזור התוכן של WebView.

הוספת אירועים של טעינת דפים לאפליקציה

יוצרים קובץ מקור חדש בכתובת lib/src/web_view_stack.dart וממלאים אותו בתוכן הבא:

lib/src/web_view_stack.dart

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

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

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

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

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

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

הקוד הזה עטף את הווידג'ט WebView בתוך Stack, ומוסיף שכבת-על של WebView עם התג LinearProgressIndicator כשאחוז טעינת הדף נמוך מ-100%. מדובר במצב של תוכנית שמשתנה עם הזמן, ולכן אחסנתם את המצב הזה במחלקה State שמשויכת ל-StatefulWidget.

כדי להשתמש בווידג'ט החדש הזה של WebViewStack, עליך לשנות את ה-lib/main.dart באופן הבא:

lib/main.dart

import 'package:flutter/material.dart';

import 'src/web_view_stack.dart';

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

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

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

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

כשתפעילו את האפליקציה, בהתאם לתנאי הרשת שלכם ואם הדפדפן שמר במטמון את הדף שאליו אתם עוברים, יוצג אינדיקטור לטעינת דף מעל אזור התוכן של WebView.

6. עבודה עם WebViewController

גישה ל-WebViewController מהווידג'ט של WebView

הווידג'ט WebView מאפשר שליטה פרוגרמטית באמצעות WebViewController. השלט רחוק הזה זמין אחרי בניית הווידג'ט WebView באמצעות קריאה חוזרת (callback). האופי האסינכרוני של זמינות נאמן המידע הזה הופך אותו למועמד העיקרי לכיתה Completer<T> האסינכרונית של דארט.

מעדכנים את 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.

הוספת פקדי ניווט ל-AppBar

עכשיו יש לך את הגרסה המעודכנת של WebViewStack, כולל NavigationControls שנוצר לאחרונה, ולכן עכשיו אפשר לשלב את כל הפריטים בגרסה המעודכנת של WebViewApp. כאן אנחנו יוצרים את WebViewController המשותף. עם WebViewApp בחלק העליון של עץ הווידג'ט באפליקציה הזו, הגיוני ליצור אותו ברמה הזו.

מעדכנים קובץ lib/main.dart באופן הבא:

lib/main.dart

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

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

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

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

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

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

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

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

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

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

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

7. מעקב אחר הניווט באמצעות התכונה 'גישה אוטומטית'

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

רישום נציג ניווט מותאם אישית

בשלב הזה, המערכת תרשום קריאה חוזרת (callback) של 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 במסמכי התיעוד של ה-API.

8. הוספת לחצן תפריט ל-AppBar

בשלבים הבאים תיצרו לחצן תפריט בווידג'ט 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'),
        ),
      ],
    );
  }
}

כשהמשתמש בוחר באפשרות ניווט ל-YouTube בתפריט, המערכת מפעילה את השיטה loadRequest של WebViewController. הניווט הזה ייחסם על ידי הקריאה החוזרת (callback) של navigationDelegate שיצרת בשלב הקודם.

כדי להוסיף את התפריט למסך של WebViewApp, צריך לשנות את lib/main.dart באופן הבא:

lib/main.dart

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

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

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

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

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

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

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

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

מפעילים את האפליקציה ומקישים על האפשרות ניווט ל-YouTube בתפריט. אמור להופיע לידך סרגל SnackBar, שיודיע לך שבקר הניווט חסם את האפשרות לנווט אל YouTube.

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

צילום מסך של אמולטור Android שפועל בו אפליקציית Flutter עם WebView מוטמע, שמציג את דף הבית של 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, אפשר להוסיף לתפריט אפשרות להשתמש ב-method runJavaScriptReturningResult.

באמצעות Editor או פעולות מקלדת מסוימות, ממירים את המחלקה (תפריט) ל-StatefulWidget. משנים את lib/src/menu.dart כך שיתאים לדברים הבאים:

lib/src/menu.dart

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

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

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

  final WebViewController controller;

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

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

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

צילום מסך של סימולטור ל-iPhone שמפעיל אפליקציית Flutter עם WebView מוטמע, שמציג את דף הבית של Flutter.dev עם אפשרויות בתפריט שבהן מוצגות האפשרויות &#39;מעבר אל YouTube&#39; או &#39;Show user-agent&#39;

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

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

ערוצי JavaScript מאפשרים לאפליקציה שלך לרשום מטפלים בקריאה חוזרת (callback) בהקשר של WebViewWidget ב-JavaScript, שניתן להפעיל כדי להעביר את הערכים בחזרה לקוד Drt של האפליקציה. בשלב הזה המערכת תרשום ערוץ 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 כדי לשלוח הודעה שמועברת ל-handler של הקריאה החוזרת 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 Channel Example (דוגמה לערוץ 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 ל-Public Address API ומחזיר את כתובת ה-IP של המכשיר. התוצאה הזו מוצגת בSnackBar על ידי הפעלת postMessage בJavascriptChannel של SnackBar.

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 למחלקה של המדינה שמתקבלת באופן הבא:

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, שכתוצאה מכך יופעלו על ידי אפשרויות בתפריט שעדיין לא נוספו.

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

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

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

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

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

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

12. לטעון נכסים, קבצים ומחרוזות HTML של Flutter ב-WebView

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

כדי לטעון קובץ שנמצא בנתיב ספציפי, צריך להוסיף את path_provider ל-pubspec.yaml. זהו פלאגין של Flutter למציאת מיקומים נפוצים במערכת הקבצים.

הריצו את הפקודה הבאה בשורת הפקודה:

$ flutter pub add path_provider

כדי לטעון את הנכס, אנחנו צריכים לציין את הנתיב לנכס ב-pubspec.yaml. ב-pubspec.yaml, מוסיפים את השורות הבאות:

pubspec.yaml

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

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

כדי להוסיף את הנכסים לפרויקט:

  1. יוצרים ספרייה חדשה בשם assets בתיקיית הבסיס של הפרויקט.
  2. יוצרים ספרייה חדשה בשם www בתיקייה assets.
  3. יוצרים ספרייה חדשה בשם styles בתיקייה www.
  4. יוצרים קובץ חדש בשם index.html בתיקייה www.
  5. יוצרים קובץ חדש בשם style.css בתיקייה styles.

מעתיקים ומדביקים את הקוד הבא בקובץ index.html:

assets/www/index.html

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

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

</body>
</html>

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

assets/www/styles/style.css

h1 {
  color: blue;
}

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

טעינת נכס של Flutter

כדי לטעון את הנכס שיצרתם, כל מה שצריך לעשות הוא להפעיל את השיטה loadFlutterAsset באמצעות WebViewController ולציין את הנתיב לנכס כפרמטר. מוסיפים את השיטה הבאה בסוף הקוד:

lib/src/menu.dart

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

טעינת קובץ מקומי

כדי לטעון קובץ במכשיר, אפשר להוסיף שיטה שתשתמש ב-method 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 יטען את הקובץ על ידי ציון הנתיב כמחרוזת שמוחזרת באמצעות ה-method _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.

הוספת האפשרויות לתפריט

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

צילום מסך של אמולטור Android שמפעיל אפליקציית Flutter עם WebView מוטמע, שמציג דף עם התווית &#39;דף הדגמה מקומי&#39; שהכותרת שלו מופיעה בצבע שחור

13. הכול מוכן!

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

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