การเพิ่ม WebView ลงในแอป Flutter

1. บทนำ

อัปเดตล่าสุด: 19-10-2021

ปลั๊กอิน WebView Flutter ช่วยให้คุณเพิ่มวิดเจ็ต WebView ลงในแอป Flutter ของ Android หรือ iOS ได้ โดยใน iOS วิดเจ็ต WebView จะได้รับการสนับสนุนจาก WKWebView ส่วนใน Android วิดเจ็ต WebView จะได้รับการสนับสนุนจาก WebView ปลั๊กอินสามารถแสดงผลวิดเจ็ต Flutter เหนือ WebView ได้ เช่น คุณสามารถแสดงเมนูแบบเลื่อนลงเหนือ WebView ได้

สิ่งที่คุณจะสร้าง

ในโค้ดแล็บนี้ คุณจะได้สร้างแอปบนอุปกรณ์เคลื่อนที่ทีละขั้นตอนโดยมี WebView โดยใช้ Flutter SDK แอปของคุณจะทำสิ่งต่อไปนี้

  • แสดงเนื้อหาเว็บใน WebView
  • แสดงวิดเจ็ต Flutter ซ้อนทับ WebView
  • ตอบสนองต่อเหตุการณ์ความคืบหน้าในการโหลดหน้าเว็บ
  • ควบคุม WebView ผ่าน WebViewController
  • บล็อกเว็บไซต์โดยใช้ NavigationDelegate
  • ประเมินนิพจน์ JavaScript
  • จัดการการเรียกกลับจาก JavaScript ด้วย JavascriptChannels
  • ตั้งค่า นำออก เพิ่ม หรือแสดงคุกกี้
  • โหลดและแสดง HTML จากชิ้นงาน ไฟล์ หรือสตริงที่มี HTML

โปรแกรมจำลอง iPhone ที่เรียกใช้แอป Flutter พร้อม WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter พร้อม WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev

สิ่งที่คุณจะได้เรียนรู้

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีใช้ปลั๊กอิน webview_flutter ในหลากหลายวิธี ซึ่งรวมถึง

  • วิธีกำหนดค่าwebview_flutterปลั๊กอิน
  • วิธีรอเหตุการณ์ความคืบหน้าในการโหลดหน้าเว็บ
  • วิธีควบคุมการไปยังส่วนต่างๆ ของหน้าเว็บ
  • วิธีสั่งให้ WebView ย้อนกลับและไปข้างหน้าในประวัติ
  • วิธีประเมิน JavaScript รวมถึงการใช้ผลลัพธ์ที่ส่งคืน
  • วิธีลงทะเบียน Callback เพื่อเรียกใช้โค้ด Dart จาก JavaScript
  • วิธีจัดการคุกกี้
  • วิธีโหลดและแสดงหน้า HTML จากชิ้นงาน ไฟล์ หรือสตริงที่มี HTML

สิ่งที่คุณต้องมี

  • Android Studio 4.1 ขึ้นไป (สำหรับการพัฒนา Android)
  • Xcode 12 ขึ้นไป (สําหรับการพัฒนา iOS)
  • Flutter SDK
  • โปรแกรมแก้ไขโค้ด เช่น Android Studio หรือ Visual Studio Code

2. ตั้งค่าสภาพแวดล้อมในการพัฒนา Flutter

คุณต้องใช้ซอฟต์แวร์ 2 อย่างเพื่อทำแล็บนี้ให้เสร็จสมบูรณ์ ได้แก่ 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 คุณจะเห็นบรรทัดในส่วนการขึ้นต่อกันสำหรับปลั๊กอิน webview_flutter

กำหนดค่า minSDK ของ Android

หากต้องการใช้ปลั๊กอิน webview_flutter ใน Android คุณต้องตั้งค่า minSDK เป็น 20 แก้ไขไฟล์ android/app/build.gradle ดังนี้

android/app/build.gradle

android {
    //...

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

4. การเพิ่มวิดเจ็ต WebView ลงในแอป Flutter

ในขั้นตอนนี้ คุณจะเพิ่ม WebView ลงในแอปพลิเคชัน WebView เป็นมุมมองในตัวที่โฮสต์ไว้ และในฐานะนักพัฒนาแอป คุณสามารถเลือกวิธีโฮสต์มุมมองในตัวเหล่านี้ในแอปได้ ใน Android คุณสามารถเลือกได้ระหว่างจอแสดงผลเสมือน ซึ่งเป็นค่าเริ่มต้นสำหรับ Android และการจัดองค์ประกอบแบบไฮบริด อย่างไรก็ตาม iOS จะใช้การจัดองค์ประกอบแบบไฮบริดเสมอ

หากต้องการดูการอภิปรายในเชิงลึกเกี่ยวกับความแตกต่างระหว่างจอแสดงผลเสมือนและการคอมโพสิตแบบไฮบริด โปรดอ่านเอกสารประกอบเกี่ยวกับการโฮสต์มุมมอง Android และ iOS ดั้งเดิมในแอป Flutter ด้วยมุมมองแพลตฟอร์ม

วาง WebView บนหน้าจอ

แทนที่เนื้อหาของ lib/main.dart ด้วยข้อมูลต่อไปนี้

lib/main.dart

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

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

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

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

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

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

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

การเรียกใช้ใน iOS หรือ Android จะแสดง WebView เป็นหน้าต่างเบราว์เซอร์แบบเต็มขอบบนอุปกรณ์ ซึ่งหมายความว่าเบราว์เซอร์จะแสดงบนอุปกรณ์แบบเต็มหน้าจอโดยไม่มีเส้นขอบหรือขอบ ขณะเลื่อน คุณจะเห็นส่วนต่างๆ ของหน้าเว็บที่อาจดูแปลกไปบ้าง เนื่องจากปิดใช้ JavaScript และการแสดงผล flutter.dev อย่างถูกต้องต้องใช้ JavaScript

การเรียกใช้แอป

เรียกใช้แอป Flutter ใน iOS หรือ Android เพื่อดู WebView ซึ่งแสดงเว็บไซต์ flutter.dev หรือจะเรียกใช้แอปในโปรแกรมจำลอง Android หรือโปรแกรมจำลอง iOS ก็ได้ คุณสามารถแทนที่ URL ของ WebView เริ่มต้นด้วยเว็บไซต์ของคุณเองได้

$ flutter run

สมมติว่าคุณมีโปรแกรมจำลองที่เหมาะสมหรือโปรแกรมจำลองที่กำลังทำงานอยู่ หรือมีอุปกรณ์จริงที่เชื่อมต่ออยู่ หลังจากคอมไพล์และติดตั้งใช้งานแอปในอุปกรณ์แล้ว คุณควรเห็นสิ่งที่คล้ายกับต่อไปนี้

โปรแกรมจำลอง iPhone ที่เรียกใช้แอป Flutter พร้อม WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter พร้อม WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev

5. รอรับเหตุการณ์การโหลดหน้าเว็บ

WebView วิดเจ็ตมีเหตุการณ์ความคืบหน้าในการโหลดหน้าเว็บหลายรายการที่แอปของคุณสามารถรับฟังได้ ในระหว่างWebViewวงจรการโหลดหน้าเว็บ จะมีเหตุการณ์การโหลดหน้าเว็บ 3 เหตุการณ์ที่แตกต่างกันซึ่งจะทริกเกอร์ ได้แก่ 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ดังนี้

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

การเพิ่มตัวควบคุมการนำทางไปยัง AppBar

เมื่อมีWebViewStackที่อัปเดตแล้วและNavigationControlsที่สร้างขึ้นใหม่ในมือ ตอนนี้ก็ถึงเวลาที่คุณจะนำทุกอย่างมารวมกันในWebViewAppที่อัปเดตแล้ว เราจะสร้างWebViewControllerที่แชร์กันตรงนี้ เมื่อมี WebViewApp ใกล้ด้านบนของโครงสร้างวิดเจ็ตในแอปนี้ การสร้างที่ระดับนี้จึงสมเหตุสมผล

อัปเดตไฟล์ lib/main.dart ดังนี้

lib/main.dart

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

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

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

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

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

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

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

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

การเรียกใช้แอปควรแสดงหน้าเว็บที่มีตัวควบคุมดังนี้

โปรแกรมจำลอง iPhone ที่เรียกใช้แอป Flutter พร้อม WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมตัวควบคุมหน้าก่อนหน้า หน้าถัดไป และการโหลดหน้าเว็บซ้ำ

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมตัวควบคุมหน้าก่อนหน้า ถัดไป และโหลดหน้าเว็บซ้ำ

7. การติดตามการนำทางด้วย NavigationDelegate

WebView จะให้ NavigationDelegate, แก่แอปของคุณ ซึ่งจะช่วยให้แอปติดตามและควบคุมการไปยังหน้าต่างๆ ของวิดเจ็ต WebView ได้ เมื่อWebView,เริ่มการนำทาง เช่น เมื่อผู้ใช้คลิกลิงก์ ระบบจะเรียกใช้ NavigationDelegate NavigationDelegateสามารถใช้การเรียกกลับเพื่อควบคุมว่า WebView จะดำเนินการนำทางต่อหรือไม่

ลงทะเบียน NavigationDelegate ที่กำหนดเอง

ในขั้นตอนนี้ คุณจะลงทะเบียนNavigationDelegateการเรียกกลับเพื่อบล็อกการไปยัง YouTube.com โปรดทราบว่าการติดตั้งใช้งานแบบง่ายนี้จะบล็อกเนื้อหา YouTube ในหน้าเว็บด้วย ซึ่งจะปรากฏในหน้าเอกสารประกอบ API ของ Flutter ต่างๆ

อัปเดต 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 ผู้อ่านควรฝึกฝนการเพิ่มตรรกะของ Callback เพื่อบล็อกการไปยังหน้าแบบเต็มไปยัง YouTube.com เท่านั้น และยังคงอนุญาตเนื้อหา YouTube ในหน้าเว็บในเอกสาร API

8. การเพิ่มปุ่มเมนูลงใน AppBar

ในอีก 2-3 ขั้นตอนถัดไป คุณจะสร้างปุ่มเมนูในวิดเจ็ต AppBar ที่ใช้ประเมิน JavaScript เรียกใช้แชแนล JavaScript และจัดการคุกกี้ โดยรวมแล้วเป็นเมนูที่มีประโยชน์จริงๆ

สร้างไฟล์ต้นฉบับใหม่ที่ lib/src/menu.dart และกรอกข้อมูลต่อไปนี้

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
}

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

  final WebViewController controller;

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

เมื่อผู้ใช้เลือกตัวเลือกเมนูไปที่ YouTube ระบบจะเรียกใช้เมธอด loadRequest ของ WebViewController navigationDelegate Callback ที่คุณสร้างไว้ในขั้นตอนก่อนหน้าจะบล็อกการนำทางนี้

หากต้องการเพิ่มเมนูลงในหน้าจอของ 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 ที่แจ้งให้ทราบว่า Navigation Controller บล็อกการไปยัง YouTube

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมรายการเมนูที่แสดงตัวเลือก &quot;ไปที่ YouTube&quot;

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมป๊อปอัปข้อความแจ้งที่อ่านว่า &quot;บล็อกการนำทางไปยัง m.youtube.com&quot;

9. การประเมิน JavaScript

WebViewController สามารถประเมินนิพจน์ JavaScript ในบริบทของหน้าเว็บปัจจุบันได้ การประเมิน JavaScript มี 2 วิธี ได้แก่ สำหรับโค้ด JavaScript ที่ไม่แสดงค่า ให้ใช้ runJavaScript และสำหรับโค้ด JavaScript ที่แสดงค่า ให้ใช้ runJavaScriptReturningResult

หากต้องการเปิดใช้ JavaScript คุณต้องกําหนดค่า WebViewController โดยตั้งค่าพร็อพเพอร์ตี้ javaScriptMode เป็น JavascriptMode.unrestricted โดยค่าเริ่มต้น ระบบจะตั้งค่า javascriptMode เป็น JavascriptMode.disabled

อัปเดต_WebViewStackState คลาสโดยเพิ่มการตั้งค่า javascriptMode ดังนี้

lib/src/web_view_stack.dart

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

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

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

ตอนนี้ WebViewWidget เรียกใช้ JavaScript ได้แล้ว คุณจึงเพิ่มตัวเลือกในเมนูเพื่อใช้วิธี runJavaScriptReturningResult ได้

แปลงคลาสเมนูเป็น StatefulWidget โดยใช้เอดิเตอร์หรือการทำงานของแป้นพิมพ์ แก้ไข lib/src/menu.dart ให้ตรงกับรายการต่อไปนี้

lib/src/menu.dart

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

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

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

  final WebViewController controller;

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

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

เมื่อแตะตัวเลือกเมนู "แสดง User-Agent" ผลลัพธ์ของการเรียกใช้ JavaScript Expression navigator.userAgent จะแสดงใน Snackbar เมื่อเรียกใช้แอป คุณอาจสังเกตเห็นว่าหน้า Flutter.dev ดูแตกต่างออกไป นี่คือผลลัพธ์ของการเรียกใช้โดยเปิดใช้ JavaScript

โปรแกรมจำลอง iPhone ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมรายการเมนูที่แสดงตัวเลือกในการ &quot;ไปยัง YouTube&quot; หรือ &quot;แสดง User-Agent&quot;

โปรแกรมจำลอง iPhone ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมป๊อปอัปข้อความแจ้งที่แสดงสตริง User-Agent

10. การทำงานกับช่อง JavaScript

ช่อง JavaScript ช่วยให้แอปของคุณลงทะเบียนตัวแฮนเดิลการเรียกกลับในบริบท JavaScript ของ WebViewWidget ซึ่งเรียกใช้เพื่อส่งค่ากลับไปยังโค้ด Dart ของแอปได้ ในขั้นตอนนี้ คุณจะลงทะเบียนช่อง SnackBar ที่จะเรียกใช้พร้อมผลลัพธ์ของ XMLHttpRequest

อัปเดตชั้นเรียน WebViewStack ดังนี้

lib/src/web_view_stack.dart

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

  final WebViewController controller;

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

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

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

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

สำหรับแต่ละแชแนล JavaScript ใน Set จะมีการสร้างออบเจ็กต์แชแนลในบริบท JavaScript เป็นพร็อพเพอร์ตี้ของออบเจ็กต์หน้าต่างที่มีชื่อเดียวกับแชแนล JavaScript name การใช้จากบริบท JavaScript เกี่ยวข้องกับการเรียก postMessage ในช่อง JavaScript เพื่อส่งข้อความที่ส่งไปยังแฮนเดิลเลอร์การเรียกกลับ onMessageReceived ของ JavascriptChannel ที่มีชื่อ

หากต้องการใช้ช่อง JavaScript ที่คุณเพิ่มไว้ก่อนหน้านี้ ให้เพิ่มรายการเมนูอีกรายการหนึ่งซึ่งเรียกใช้ XMLHttpRequest ในบริบท JavaScript และส่งผลลัพธ์กลับโดยใช้ช่อง JavaScript SnackBar

ตอนนี้ WebViewWidget รู้จักช่อง JavaScript ของเรา,แล้ว คุณจะเพิ่มตัวอย่างเพื่อขยายแอปเพิ่มเติม โดยทำได้ด้วยการเพิ่ม PopupMenuItem พิเศษลงในคลาส Menu และเพิ่มฟังก์ชันการทำงานพิเศษ

อัปเดต _MenuOptions ด้วยตัวเลือกเมนูเพิ่มเติมโดยการเพิ่มค่าการแจงนับ javascriptChannel และเพิ่มการติดตั้งใช้งานลงในคลาส Menu ดังนี้

lib/src/menu.dart

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

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

  final WebViewController controller;

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

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

ระบบจะเรียกใช้ JavaScript นี้เมื่อผู้ใช้เลือกตัวเลือกเมนูตัวอย่างช่อง JavaScript

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

โค้ดนี้จะส่งGETคำขอไปยัง API ที่อยู่ IP สาธารณะ ซึ่งจะแสดงที่อยู่ IP ของอุปกรณ์ ผลลัพธ์นี้จะแสดงใน SnackBar โดยการเรียกใช้ postMessage ใน SnackBar JavascriptChannel

11. การจัดการคุกกี้

แอปของคุณจัดการคุกกี้ใน WebView ได้โดยใช้คลาส CookieManager ในขั้นตอนนี้ คุณจะแสดงรายการคุกกี้ ล้างรายการคุกกี้ ลบคุกกี้ และตั้งค่าคุกกี้ใหม่ เพิ่มรายการใน _MenuOptions สำหรับ Use Case ของคุกกี้แต่ละรายการดังนี้

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

_MenuState จะมีโค้ดที่เพิ่มไว้ก่อนหน้านี้ในคลาส Menu พร้อมกับ CookieManager ที่เพิ่มใหม่ ในชุดส่วนถัดไป คุณจะเพิ่มฟังก์ชันตัวช่วยลงใน _MenuState ซึ่งจะเรียกใช้รายการเมนูที่ยังไม่ได้เพิ่ม

ดูรายการคุกกี้ทั้งหมด

คุณจะใช้ JavaScript เพื่อรับรายการคุกกี้ทั้งหมด โดยให้เพิ่มเมธอดตัวช่วยที่ส่วนท้ายของคลาส _MenuState ชื่อ _onListCookies เมื่อใช้เมธอด runJavaScriptReturningResult เมธอดตัวช่วยจะเรียกใช้ document.cookie ในบริบท JavaScript ซึ่งจะแสดงรายการคุกกี้ทั้งหมด

เพิ่มรายการต่อไปนี้ลงในคลาส _MenuState

lib/src/menu.dart

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

ล้างคุกกี้ทั้งหมด

หากต้องการล้างคุกกี้ทั้งหมดใน WebView ให้ใช้วิธี clearCookies ของคลาส CookieManager เมธอดจะแสดง Future<bool> ที่จะเปลี่ยนเป็น true หาก CookieManager ล้างคุกกี้ และ false หากไม่มีคุกกี้ให้ล้าง

เพิ่มรายการต่อไปนี้ลงในคลาส _MenuState

lib/src/menu.dart

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

คุณเพิ่มคุกกี้ได้โดยเรียกใช้ JavaScript API ที่ใช้เพื่อเพิ่มคุกกี้ลงในเอกสาร JavaScript มีคำอธิบายโดยละเอียดใน MDN

เพิ่มรายการต่อไปนี้ลงในคลาส _MenuState

lib/src/menu.dart

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

นอกจากนี้ยังตั้งค่าคุกกี้ได้โดยใช้ CookieManager ดังนี้

เพิ่มรายการต่อไปนี้ลงในคลาส _MenuState

lib/src/menu.dart

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

การนำคุกกี้ออกเกี่ยวข้องกับการเพิ่มคุกกี้โดยกำหนดวันที่หมดอายุเป็นวันในอดีต

เพิ่มรายการต่อไปนี้ลงในคลาส _MenuState

lib/src/menu.dart

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

การเพิ่มรายการเมนู CookieManager

สิ่งที่คุณต้องทำคือเพิ่มตัวเลือกเมนูและเชื่อมโยงตัวเลือกเหล่านั้นกับเมธอดตัวช่วยที่คุณเพิ่งเพิ่ม อัปเดตชั้นเรียน _MenuState ดังนี้

lib/src/menu.dart

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

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

การใช้ CookieManager

หากต้องการใช้ฟังก์ชันทั้งหมดที่คุณเพิ่งเพิ่มลงในแอป ให้ลองทำตามขั้นตอนต่อไปนี้

  1. เลือกแสดงรายการคุกกี้ โดยควรแสดงคุกกี้ Google Analytics ที่ flutter.dev ตั้งค่าไว้
  2. เลือกล้างคุกกี้ ซึ่งควรรายงานว่าได้ล้างคุกกี้แล้ว
  3. เลือกล้างคุกกี้อีกครั้ง โดยควรรายงานว่าไม่มีคุกกี้ที่ล้างได้
  4. เลือกแสดงรายการคุกกี้ ซึ่งควรรายงานว่าไม่มีคุกกี้
  5. เลือกเพิ่มคุกกี้ โดยควรรายงานว่าได้เพิ่มคุกกี้แล้ว
  6. เลือกตั้งค่าคุกกี้ โดยควรรายงานว่ามีการตั้งค่าคุกกี้แล้ว
  7. เลือกแสดงคุกกี้ แล้วเลือกนำคุกกี้ออก

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมรายการตัวเลือกเมนูที่ครอบคลุมการไปยัง YouTube, การแสดง User Agent และการโต้ตอบกับที่เก็บคุกกี้ของเบราว์เซอร์

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมกล่องโต้ตอบแบบ Toast ที่แสดงคุกกี้ที่ตั้งค่าไว้ในเบราว์เซอร์

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมกล่องโต้ตอบแบบ Toast ที่ระบุว่า &quot;มีคุกกี้&quot; ตอนนี้มันหายไปแล้ว&quot;

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าแรกของ Flutter.dev พร้อมกล่องโต้ตอบแบบ Toast ที่ระบุว่า &quot;เพิ่มคุกกี้ที่กำหนดเองแล้ว&quot;

12. โหลดชิ้นงาน ไฟล์ และสตริง HTML ของ Flutter ใน WebView

แอปสามารถโหลดไฟล์ HTML ได้โดยใช้วิธีการต่างๆ และแสดงไฟล์เหล่านั้นใน WebView ในขั้นตอนนี้ คุณจะโหลดชิ้นงาน Flutter ที่ระบุไว้ในไฟล์ pubspec.yaml โหลดไฟล์ที่อยู่ในเส้นทางที่ระบุ และโหลดหน้าเว็บโดยใช้สตริง HTML

หากต้องการโหลดไฟล์ที่อยู่ในเส้นทางที่ระบุ คุณจะต้องเพิ่ม path_provider ลงใน pubspec.yaml นี่คือปลั๊กอิน Flutter สำหรับค้นหาตำแหน่งที่ใช้กันทั่วไปในระบบไฟล์

เรียกใช้คำสั่งต่อไปนี้ในบรรทัดคำสั่ง

$ flutter pub add path_provider

หากต้องการโหลดชิ้นงาน เราต้องระบุเส้นทางไปยังชิ้นงานใน pubspec.yaml ใน pubspec.yaml ให้เพิ่มบรรทัดต่อไปนี้

pubspec.yaml

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

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

หากต้องการเพิ่มชิ้นงานลงในโปรเจ็กต์ ให้ทำตามขั้นตอนต่อไปนี้

  1. สร้างไดเรกทอรีใหม่โดยใช้ชื่อ assets ในโฟลเดอร์รูทของโปรเจ็กต์
  2. สร้างไดเรกทอรีใหม่ชื่อ www ในโฟลเดอร์ assets
  3. สร้างไดเรกทอรีใหม่ชื่อ styles ในโฟลเดอร์ www
  4. สร้างไฟล์ใหม่ชื่อ index.html ในโฟลเดอร์ www
  5. สร้างไฟล์ใหม่ชื่อ style.css ในโฟลเดอร์ styles

คัดลอกและวางโค้ดต่อไปนี้ในไฟล์ index.html

assets/www/index.html

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

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

</body>
</html>

สำหรับ style.css ให้ใช้ 2-3 บรรทัดต่อไปนี้เพื่อตั้งค่าสไตล์ส่วนหัว HTML

assets/www/styles/style.css

h1 {
  color: blue;
}

เมื่อตั้งค่าชิ้นงานและพร้อมใช้งานแล้ว คุณจะติดตั้งใช้งานเมธอดที่จำเป็นสำหรับการโหลดและแสดงชิ้นงาน ไฟล์ หรือสตริง HTML ของ Flutter ได้

โหลดชิ้นงาน Flutter

หากต้องการโหลดชิ้นงานที่เพิ่งสร้าง สิ่งที่คุณต้องทำคือเรียกใช้เมธอด loadFlutterAsset โดยใช้ WebViewController และระบุเส้นทางไปยังชิ้นงานเป็นพารามิเตอร์ เพิ่มเมธอดต่อไปนี้ที่ส่วนท้ายของโค้ด

lib/src/menu.dart

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

โหลดไฟล์ในเครื่อง

สำหรับการโหลดไฟล์ในอุปกรณ์ คุณสามารถเพิ่มเมธอดที่จะใช้เมธอด loadFile ได้อีกครั้งโดยใช้ WebViewController ซึ่งรับ String ที่มีเส้นทางไปยังไฟล์

คุณต้องสร้างไฟล์ที่มีโค้ด HTML ก่อน โดยทำได้ด้วยการเพิ่มโค้ด HTML เป็นสตริงที่ด้านบนของโค้ดในไฟล์ menu.dart ใต้การนำเข้า

lib/src/menu.dart

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

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

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

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

หากต้องการสร้าง File และเขียนสตริง HTML ลงในไฟล์ คุณจะต้องเพิ่ม 2 วิธี _onLoadLocalFileExample จะโหลดไฟล์โดยระบุเส้นทางเป็นสตริงซึ่งเมธอด _prepareLocalFile() จะแสดงผล เพิ่มเมธอดต่อไปนี้ลงในโค้ด

lib/src/menu.dart

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

  await controller.loadFile(pathToIndex);
}

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

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

  return indexFile.path;
}

โหลดสตริง HTML

การแสดงหน้าเว็บโดยระบุสตริง HTML นั้นทำได้ง่ายๆ WebViewController มีเมธอดที่คุณใช้ได้ชื่อ loadHtmlString ซึ่งคุณสามารถระบุสตริง HTML เป็นอาร์กิวเมนต์ได้ จากนั้น WebView จะแสดงหน้า HTML ที่ระบุ เพิ่มวิธีการต่อไปนี้ลงในโค้ด

lib/src/menu.dart

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

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

  await controller.loadFile(pathToIndex);
}

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

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

  return indexFile.path;
}

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

เพิ่มรายการในเมนู

เมื่อตั้งค่าชิ้นงานและพร้อมใช้งานแล้ว รวมถึงสร้างวิธีการที่มีฟังก์ชันทั้งหมดแล้ว ก็จะอัปเดตเมนูได้ เพิ่มรายการต่อไปนี้ลงใน _MenuOptions enum

lib/src/menu.dart

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

ตอนนี้คุณอัปเดต Enum แล้ว จึงเพิ่มตัวเลือกเมนูและเชื่อมโยงกับเมธอด Helper ที่เพิ่งเพิ่มได้ อัปเดตชั้นเรียน _MenuState ดังนี้

lib/src/menu.dart

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

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

การทดสอบชิ้นงาน ไฟล์ และสตริง HTML

หากต้องการทดสอบว่าโค้ดที่คุณเพิ่งติดตั้งใช้งานทํางานหรือไม่ คุณสามารถเรียกใช้โค้ดในอุปกรณ์และคลิกรายการเมนูที่เพิ่งเพิ่มใหม่รายการใดรายการหนึ่ง โปรดสังเกตว่า _onLoadFlutterAssetExample ใช้ style.css ที่เราเพิ่มเพื่อเปลี่ยนส่วนหัวของไฟล์ HTML เป็นสีน้ำเงิน

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าเว็บที่มีป้ายกำกับว่า &quot;หน้าเดโมในเครื่อง&quot; โดยมีชื่อเป็นสีน้ำเงิน

โปรแกรมจำลอง Android ที่เรียกใช้แอป Flutter ที่มี WebView แบบฝังซึ่งแสดงหน้าเว็บที่มีป้ายกำกับว่า &quot;หน้าเว็บเดโมในเครื่อง&quot; โดยมีชื่อเป็นสีดำ

13. เสร็จเรียบร้อย

ขอแสดงความยินดี! คุณทำ Codelab เสร็จแล้ว คุณดูโค้ดที่เสร็จสมบูรณ์แล้วสำหรับ Codelab นี้ได้ในที่เก็บ Codelab

ดูข้อมูลเพิ่มเติมได้ที่โค้ดแล็บ Flutter อื่นๆ