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

1. บทนำ

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

คุณเพิ่มวิดเจ็ต WebView ลงในแอป Flutter สำหรับ Android หรือ iOS ได้ด้วยปลั๊กอิน WebView Flutter ใน 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 หรือ 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 เป็นทรัพยากร Dependency

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

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

หากคุณตรวจสอบ pubspec.yaml คุณจะเห็นบรรทัดในส่วนทรัพยากร 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 ผ่าน Callback ลักษณะอะซิงโครนัสของความพร้อมใช้งานตัวควบคุมนี้ทำให้เป็นตัวเลือกหลักสำหรับคลาส 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 ที่กำหนดเอง

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

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

lib/src/web_view_stack.dart

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

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

  final WebViewController controller;

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

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

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

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

ในขั้นตอนต่อไป คุณจะต้องเพิ่มรายการในเมนูเพื่อเปิดใช้การทดสอบ NavigationDelegate โดยใช้คลาส WebViewController จึงเป็นเพียงแบบฝึกหัดสำหรับผู้อ่านเพื่อเพิ่มตรรกะของ 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 การนำทางนี้จะถูกบล็อกโดย 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 ซึ่งมี WebView ฝังอยู่ ซึ่งแสดงให้เห็นหน้าแรกของ 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 เมธอด Help ของคุณจะเรียกใช้ 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. เลือก Set cookie ระบบจะรายงานคุกกี้ตามที่ตั้งค่าไว้
  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;
}

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

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

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