การเพิ่ม 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
  • จัดการ Callback จาก JavaScript ด้วย JavascriptChannels
  • ตั้งค่า นำออก เพิ่ม หรือแสดงคุกกี้
  • โหลดและแสดง HTML จากเนื้อหา ไฟล์ หรือสตริงที่มี HTML

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

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

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

ใน Codelab นี้ คุณจะได้เรียนรู้วิธีใช้ปลั๊กอิน 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 หรือ Emacs

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

คุณต้องใช้ซอฟต์แวร์ 2 อย่างในการฝึกนี้ ได้แก่ Flutter SDK และเครื่องมือแก้ไข

คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้

3. เริ่มต้นใช้งาน

การเริ่มต้นใช้งาน Flutter

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

$ 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 คุณจะเห็นบรรทัดในส่วนทรัพยากร Dependency สำหรับปลั๊กอิน 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 มีเหตุการณ์การโหลดหน้าเว็บ 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

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 callback เพื่อบล็อกการไปยัง YouTube.com โปรดทราบว่าการใช้งานที่ง่ายดายนี้ยังบล็อกเนื้อหา YouTube ในบรรทัดด้วย ซึ่งปรากฏในหน้าเอกสารประกอบของ Flutter API ต่างๆ

อัปเดต lib/src/web_view_stack.dart ดังนี้

lib/src/web_view_stack.dart

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

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

  final WebViewController controller;

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

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

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

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

ในขั้นตอนถัดไป คุณจะเพิ่มรายการเมนูเพื่อเปิดใช้การทดสอบ NavigationDelegate โดยใช้คลาส WebViewController คุณสามารถลองเพิ่มตรรกะของคอลแบ็กเพื่อบล็อกเฉพาะการไปยังหน้า YouTube.com แบบเต็มหน้า และยังคงอนุญาตให้แสดงเนื้อหา YouTube ในบรรทัดในเอกสารประกอบ API

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

ในอีก 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 การนำทางนี้จะถูกบล็อกโดย 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 พร้อมรายการเมนูที่แสดงตัวเลือก &quot;ไปยัง YouTube&quot;

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

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

WebViewController สามารถประเมินนิพจน์ JavaScript ในบริบทของหน้าปัจจุบัน การประเมิน JavaScript มี 2 วิธี ได้แก่ ใช้ 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 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 ทำให้แอปของคุณสามารถลงทะเบียนตัวแฮนเดิล Callback ในบริบท JavaScript ของ WebViewWidget ซึ่งสามารถเรียกใช้เพื่อถ่ายทอดค่ากลับไปยังโค้ด Dart ของแอปได้ ในขั้นตอนนี้ คุณจะต้องลงทะเบียนช่อง SnackBar ซึ่งระบบจะเรียกใช้ด้วยผลลัพธ์ของ XMLHttpRequest

อัปเดตคลาส WebViewStack ดังนี้

lib/src/web_view_stack.dart

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

  final WebViewController controller;

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

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

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

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

สำหรับแชแนล JavaScript แต่ละแชแนลใน Set ออบเจ็กต์แชแนลจะใช้ได้ในบริบทของ JavaScript เป็นพร็อพเพอร์ตี้หน้าต่างที่มีชื่อเดียวกับแชแนล JavaScript name การใช้สิ่งนี้จากบริบท JavaScript จะเกี่ยวข้องกับการเรียกใช้ postMessage บนแชแนล JavaScript เพื่อส่งข้อความที่ส่งไปยังเครื่องจัดการ Callback 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 ใน JavascriptChannel ของ SnackBar

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

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

lib/src/menu.dart

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

การเปลี่ยนแปลงที่เหลือในขั้นตอนนี้จะมุ่งเน้นที่คลาส Menu ซึ่งรวมถึงการเปลี่ยนคลาส Menu จากแบบไม่มีสถานะเป็นแบบมีสถานะ การเปลี่ยนแปลงนี้สำคัญเนื่องจาก Menu จำเป็นต้องเป็นเจ้าของ CookieManager และสถานะที่เปลี่ยนแปลงได้ในวิดเจ็ตแบบไม่มีสถานะเป็นส่วนผสมที่ไม่เหมาะสม

เพิ่ม CookieManager ลงในคลาส 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 เพื่อรับรายการคุกกี้ทั้งหมด เพื่อให้บรรลุเป้าหมายนี้ ให้เพิ่มเมธอด Help ในตอนท้ายของคลาส _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

แค่เพิ่มตัวเลือกเมนู แล้วต่อสายเข้ากับวิธีการของ 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:                        // 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 พร้อมข้อความโทสต์ป๊อปอัปแสดงคุกกี้ที่ตั้งไว้ในเบราว์เซอร์

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

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

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

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

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

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

$ flutter pub add path_provider

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

pubspec.yaml

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

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

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

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

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

assets/www/index.html

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

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

</body>
</html>

สำหรับ style.css ให้ใช้บรรทัดต่อไปนี้ในการตั้งค่ารูปแบบส่วนหัว HTML:

assets/www/styles/style.css

h1 {
  color: blue;
}

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

โหลดเนื้อหา Flutter

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

lib/src/menu.dart

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

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

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

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

lib/src/menu.dart

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

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

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

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

หากต้องการสร้าง File และเขียนสตริง HTML ลงในไฟล์ คุณจะต้องเพิ่มเมธอด 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.

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

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