將 WebView 加入 Flutter 應用程式

1. 簡介

上次更新時間:2021 年 10 月 19 日

有了 WebView Flutter 外掛程式,您就能在 Android 或 iOS Flutter 應用程式中新增 WebView 小工具。iOS 版 WebView 小工具由 WKWebView 支援,而 Android 版 WebView 小工具則由 WebView 支援。這個外掛程式可在網頁檢視畫面中算繪 Flutter 小工具。例如,您可以在網頁檢視畫面中顯示下拉式選單。

建構項目

在本程式碼研究室中,您將使用 Flutter SDK 逐步建構 WebView。您的應用程式將會:

  • WebView中顯示網路內容
  • 顯示堆疊在 WebView 上的 Flutter 小工具
  • 回應網頁載入進度事件
  • 透過 WebViewController 控制 WebView
  • 使用 NavigationDelegate 封鎖網站
  • 評估 JavaScript 運算式
  • 使用 JavascriptChannels 處理 JavaScript 的回呼
  • 設定、移除、新增或顯示 Cookie
  • 從含有 HTML 的素材資源、檔案或字串中載入並顯示 HTML

iPhone 模擬器執行 Flutter 應用程式的螢幕截圖,畫面中有內嵌的 WebView 顯示 Flutter.dev 首頁

Android 模擬器執行 Flutter 應用程式的螢幕截圖,當中有內嵌 WebView 顯示 Flutter.dev 首頁

課程內容

在本程式碼研究室中,您將瞭解如何透過多種方式使用 webview_flutter 外掛程式,包括:

  • 如何設定 webview_flutter 外掛程式
  • 如何監聽網頁載入進度事件
  • 如何控制網頁導覽
  • 如何指令 WebView 在歷史記錄中向前及向前快轉
  • 如何評估 JavaScript,包括使用傳回的結果
  • 如何註冊回呼,以從 JavaScript 呼叫 Dart 程式碼
  • 如何管理 Cookie
  • 如何從素材資源、檔案或包含 HTML 的字串,載入並顯示 HTML 網頁

軟硬體需求

2. 設定 Flutter 開發環境

您需要使用兩項軟體:Flutter SDK編輯器

您可以使用下列任一裝置執行程式碼研究室:

3. 開始使用

開始使用 Flutter

建立新 Flutter 專案的方式有很多種,Android StudioVisual Studio Code 都提供了這項工作所需的工具。請按照連結的程序建立專案,或是在便利的指令列終端機中執行下列指令。

$ flutter create --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Resolving dependencies in `webview_in_flutter`... 
Downloading packages... 
Got dependencies in `webview_in_flutter`.
Wrote 74 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

Your application code is in webview_in_flutter/lib/main.dart.

將 WebView Flutter 外掛程式新增為依附元件

使用 Pub 套件輕鬆為 Flutter 應用程式新增額外功能。在這個程式碼研究室中,您要在專案中新增 webview_flutter 外掛程式。在終端機中執行下列指令。

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

如果您檢查 pubspec.yaml,您會發現 webview_flutter 外掛程式的依附元件區段中有一行文字。

設定 Android minSDK

如要在 Android 上使用 webview_flutter 外掛程式,您必須將 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. 在 Flutter 應用程式中加入 WebView 小工具

在這個步驟中,您將在應用程式中新增 WebView。WebView 是代管式的原生檢視畫面,應用程式開發人員可以選擇如何在應用程式中代管這些原生檢視畫面。在 Android 上,你有選擇虛擬螢幕 (目前 Android 預設) 和混合組合。不過,iOS 一律使用混合型組合。

如要深入瞭解虛擬顯示器和混合型組合之間的差異,請參閱使用 Platform View 在 Flutter 應用程式中代管原生 Android 和 iOS 檢視畫面相關說明文件。

將 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 後,WebView 會在裝置上顯示為全出血的瀏覽器視窗,因此瀏覽器會以全螢幕模式顯示裝置,不會有任何邊框或邊界。捲動畫面時,你可能會發現網頁某些部分看起來有點奇怪。這是因為目前 JavaScript 已停用,必須啟用 JavaScript 才能正確轉譯 flutter.dev

執行應用程式

在 iOS 或 Android 中執行 Flutter 應用程式,即可查看顯示 flutter.dev 網站的 WebView。或者,在 Android 模擬器或 iOS 模擬器中執行應用程式。您可以隨意將初始的 WebView 網址替換為您自己的網站。

$ flutter run

假設您已執行適當的模擬器或模擬器,或是已連接實體裝置,在編譯並部署應用程式後,畫面應會顯示如下的內容:

iPhone 模擬器執行 Flutter 應用程式的螢幕截圖,畫面中有內嵌的 WebView 顯示 Flutter.dev 首頁

Android 模擬器執行 Flutter 應用程式的螢幕截圖,當中有內嵌 WebView 顯示 Flutter.dev 首頁

5. 監聽網頁載入事件

WebView 小工具提供多個網頁載入進度事件,供應用程式監聽。在 WebView 網頁載入循環中,會觸發三種不同的網頁載入事件:onPageStartedonProgressonPageFinished。在這個步驟中,您將實作網頁載入指標。此外,這會顯示您可以在 WebView 內容區域轉譯 Flutter 內容。

在應用程式中加入網頁載入事件

lib/src/web_view_stack.dart 中建立新的來源檔案,並在該檔案中填入下列內容:

lib/src/web_view_stack.dart

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

class WebViewStack extends StatefulWidget {
  const WebViewStack({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 中,當網頁載入百分比低於 100% 時,有條件地將 WebViewLinearProgressIndicator 重疊。由於這涉及會隨著時間變更的程式狀態,所以您已將此狀態儲存在與 StatefulWidget 相關聯的 State 類別中。

如要使用這個新的 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

透過 WebView 小工具存取 WebViewController

WebView 小工具透過 WebViewController 提供程式輔助控制選項。這個控制器可在透過回呼建構 WebView 小工具後使用。這個控制器的可用性非同步性質,因此成為 Dart 非同步 Completer<T> 類別的最佳候選者。

依照下列方式更新 lib/src/web_view_stack.dart

lib/src/web_view_stack.dart

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

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

  final WebViewController controller;                        // ADD

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

class _WebViewStackState extends State<WebViewStack> {
  var loadingPercentage = 0;
  // REMOVE the controller that was here

  @override
  void initState() {
    super.initState();
    // Modify from here...
    widget.controller.setNavigationDelegate(
      NavigationDelegate(
        onPageStarted: (url) {
          setState(() {
            loadingPercentage = 0;
          });
        },
        onProgress: (progress) {
          setState(() {
            loadingPercentage = progress;
          });
        },
        onPageFinished: (url) {
          setState(() {
            loadingPercentage = 100;
          });
        },
      ),
    );
    // ...to here.
  }

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

WebViewStack 小工具現在會使用在周圍小工具中建立的控制器。這樣就能輕鬆與應用程式的其他部分共用 WebViewWidget 的控制器。

編寫導覽控制項

確保 WebView 正常運作是一回事,但如果能在網頁記錄中來回瀏覽,再重新載入網頁,是非常實用的做法。幸好,使用 WebViewController 即可在應用程式中加入這項功能。

lib/src/navigation_controls.dart 中建立新的來源檔案,並填入以下內容:

lib/src/navigation_controls.dart

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

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

  final WebViewController controller;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        IconButton(
          icon: const Icon(Icons.arrow_back_ios),
          onPressed: () async {
            final messenger = ScaffoldMessenger.of(context);
            if (await controller.canGoBack()) {
              await controller.goBack();
            } else {
              messenger.showSnackBar(
                const SnackBar(content: Text('No back history item')),
              );
              return;
            }
          },
        ),
        IconButton(
          icon: const Icon(Icons.arrow_forward_ios),
          onPressed: () async {
            final messenger = ScaffoldMessenger.of(context);
            if (await controller.canGoForward()) {
              await controller.goForward();
            } else {
              messenger.showSnackBar(
                const SnackBar(content: Text('No forward history item')),
              );
              return;
            }
          },
        ),
        IconButton(
          icon: const Icon(Icons.replay),
          onPressed: () {
            controller.reload();
          },
        ),
      ],
    );
  }
}

這個小工具會在建構期間使用與此小工具共用的 WebViewController,讓使用者透過一系列的 IconButton 控制 WebView

在 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, 啟動導覽 (例如當使用者點選連結時),系統會呼叫 NavigationDelegateNavigationDelegate 回呼可用來控制 WebView 是否繼續導覽。

註冊自訂 NavigationDelegate

在這個步驟中,您將註冊 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,
          ),
      ],
    );
  }
}

在下一個步驟中,您將新增選單項目,以使用 WebViewController 類別測試 NavigationDelegate。這可以作為練習者的練習,加強回呼的邏輯,僅封鎖前往 YouTube.com 的整頁導覽,同時仍允許 API 說明文件中內嵌 YouTube 內容。

8. 將選單按鈕新增至 AppBar

在接下來的幾個步驟中,您將在 AppBar 小工具中製作選單按鈕,用來評估 JavaScript、叫用 JavaScript 管道及管理 Cookie。總之,提供實用的菜單。

lib/src/menu.dart 中建立新的來源檔案,並填入以下內容:

lib/src/menu.dart

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

enum _MenuOptions {
  navigationDelegate,
}

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

  final WebViewController controller;

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

使用者選取「前往 YouTube」選單選項時,系統會執行 WebViewControllerloadRequest 方法。您在上一個步驟中建立的 navigationDelegate 回呼會封鎖這項導覽。

如要將選單新增至 WebViewApp 的螢幕,請按照下列方式修改 lib/main.dart

lib/main.dart

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

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

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

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

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

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

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

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

執行應用程式,然後輕觸「Navigate to YouTube」選單項目。系統應該會播放 SnackBar,通知您導覽控制器封鎖了導覽至 YouTube。

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示 Flutter.dev 首頁,其中選單項目顯示「Navigate to YouTube」選項

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中有內嵌的 WebView 顯示 Flutter.dev 首頁,旁邊彈出浮動式訊息「封鎖導覽至 m.youtube.com」

9. 評估 JavaScript

WebViewController 可以根據目前網頁的內容評估 JavaScript 運算式。評估 JavaScript 的方法有兩種:如果是未傳回值的 JavaScript 程式碼,請使用 runJavaScript;如果是傳回值的 JavaScript 程式碼,請使用 runJavaScriptReturningResult

如要啟用 JavaScript,您必須在 javaScriptMode 屬性設為 JavascriptMode.unrestricted 的情況下設定 WebViewController。根據預設,javascriptMode 設為 JavascriptMode.disabled

新增 javascriptMode 設定以更新 _WebViewStackState 類別,如下所示:

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.
      ],
    );
  }
}

輕觸 [顯示使用者代理程式]選單選項,執行 JavaScript 運算式 navigator.userAgent 的結果會顯示在 Snackbar 中。執行應用程式時,您可能會注意到 Flutter.dev 頁面看起來不太一樣。這是在啟用 JavaScript 的情況下執行的結果。

iPhone 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示 Flutter.dev 首頁,其中選單項目顯示「Navigate to YouTube」(前往 YouTube) 選項或「顯示使用者代理程式」

iPhone 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示 Flutter.dev 首頁,旁邊彈出浮動式訊息,其中顯示使用者代理程式字串。

10. 使用 JavaScript 頻道

JavaScript 管道可讓應用程式在 WebViewWidget 的 JavaScript 環境中註冊回呼處理常式,系統就能叫用這些處理常式,將值傳回應用程式的 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,
          ),
      ],
    );
  }
}

對於 Set 中的每個 JavaScript 管道,您可以在 JavaScript 環境中提供管道物件,做為視窗屬性,名稱與 JavaScript 頻道 name 相同。如要從 JavaScript 環境使用這個項目,需要在 JavaScript 管道上呼叫 postMessage,以將訊息傳遞至已命名 JavascriptChannelonMessageReceived 回呼處理常式。

如要使用上述新增的 JavaScript 頻道,請新增其他選單項目,以便在 JavaScript 環境中執行 XMLHttpRequest,並使用 SnackBar JavaScript 管道傳回結果。

現在 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 要求傳送至 Public IP Address API,傳回裝置的 IP 位址。在 SnackBar JavascriptChannel 叫用 postMessage,這個結果會顯示在 SnackBar 中。

11. 管理 Cookie

您的應用程式可以使用 CookieManager 類別管理 WebView 中的 Cookie。在這個步驟中,您將顯示 Cookie 清單、清除 Cookie 清單、刪除 Cookie,以及設定新 Cookie。針對每個 Cookie 用途,將項目新增至 _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 新增輔助函式,而該函式又會叫用 尚未新增的選單項目。

取得完整 Cookie 清單

您將使用 JavaScript 取得所有 Cookie 的清單。為此,請在 _MenuState 類別的結尾新增名為 _onListCookies 的輔助方法。使用 runJavaScriptReturningResult 方法時,輔助方法會在 JavaScript 環境中執行 document.cookie,傳回所有 Cookie 的清單。

將以下內容新增至 _MenuState 類別:

lib/src/menu.dart

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

清除所有 Cookie

如要清除 WebView 中的所有 Cookie,請使用 CookieManager 類別的 clearCookies 方法。如果 CookieManager 已清除 Cookie,此方法會傳回 Future<bool> 解析為 true;如果沒有可清除的 Cookie,則傳回 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 即可新增 Cookie。深入瞭解如何將 Cookie 加入 JavaScript 文件的 API。

將以下內容新增至 _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 設定 Cookie,如下所示。

將以下內容新增至 _MenuState 類別:

lib/src/menu.dart

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

移除 Cookie 時,您必須新增 Cookie,並將到期日設在過去。

將以下內容新增至 _MenuState 類別:

lib/src/menu.dart

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

新增 CookieManager 選單項目

接下來只要新增選單選項,再連接到剛才新增的輔助方法即可。請更新 _MenuState 類別,如下所示:

lib/src/menu.dart

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

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

執行 CookieManager

如要使用您剛才新增的所有功能,請嘗試下列步驟:

  1. 選取「列出 Cookie」。其中應該會列出 flutter.dev 設定的 Google Analytics Cookie。
  2. 選取「清除 Cookie」。這樣應該就會回報 Cookie 確實已清除。
  3. 再次選取「清除 Cookie」。系統應會回報沒有可清除的 Cookie。
  4. 選取「列出 Cookie」。系統應該會回報沒有 Cookie。
  5. 選取「新增 Cookie」。將 Cookie 記錄為已加入。
  6. 選取「設定 Cookie」。將 Cookie 記錄為已設定。
  7. 選取「列出 Cookie」,最後再選取「移除 Cookie」

螢幕截圖:Android 模擬器執行 Flutter 應用程式,其中內嵌 WebView 顯示 Flutter.dev 首頁,其中選單選項包括前往 YouTube、顯示使用者代理程式,以及與瀏覽器的 Cookie jar 互動

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示 Flutter.dev 首頁和浮動式訊息,顯示瀏覽器中設定的 Cookie

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示 Flutter.dev 首頁,浮動式訊息彈出式視窗寫著「有 Cookie。如今他們都走了!」

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中嵌入的 WebView 顯示 Flutter.dev 首頁,旁邊有顯示「已新增自訂 Cookie」的浮動式訊息。

12. 在 WebView 中載入 Flutter 資產、檔案和 HTML 字串

應用程式可以使用不同方法載入 HTML 檔案,並在 WebView 中顯示這些檔案。在這個步驟中,您將載入 pubspec.yaml 檔案中指定的 Flutter 素材資源,並載入位於指定路徑的檔案,並使用 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. 在專案的根資料夾中新建名為 assetsDirectory
  2. assets 資料夾中建立名為 www 的新目錄
  3. www 資料夾中建立名為 styles 的新目錄
  4. www 資料夾中建立名為 index.html 的新檔案
  5. styles 資料夾中建立名為 style.css 的新檔案

複製下列程式碼並貼到 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 資產

載入剛才建立的素材資源時,只需使用 WebViewController 呼叫 loadFlutterAsset 方法,並將做為素材資源的路徑提供給參數即可。在程式碼結尾新增以下方法:

lib/src/menu.dart

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

載入本機檔案

如要在裝置上載入檔案,您可以新增使用 loadFile 方法的方法,再次使用 WebViewController,此方法會採用含有檔案路徑的 String

您必須先建立包含 HTML 程式碼的檔案。方法很簡單,只要在匯入作業下方的 menu.dart 檔案中,將 HTML 程式碼做為程式碼頂端的字串加到程式碼頂端即可。

lib/src/menu.dart

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

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

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

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

如要建立 File 並將 HTML 字串寫入檔案,您需要新增兩種方法。_onLoadLocalFileExample 會以字串形式提供路徑,以載入檔案,而 _prepareLocalFile() 方法會傳回該路徑。在程式碼中加入以下方法:

lib/src/menu.dart

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

  await controller.loadFile(pathToIndex);
}

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

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

  return indexFile.path;
}

載入 HTML 字串

以提供 HTML 字串的方式顯示網頁是直接了當的。WebViewController 提供名為 loadHtmlString 的方法,可讓您提供 HTML 字串做為引數。WebView 會顯示提供的 HTML 網頁。在程式碼中加入以下方法:

lib/src/menu.dart

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

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

  await controller.loadFile(pathToIndex);
}

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

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

  return indexFile.path;
}

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

新增菜單品項

現在素材資源已設定完成且可供使用,且含有所有功能的方法後,就可以更新選單。將下列項目新增至 _MenuOptions 列舉:

lib/src/menu.dart

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

列舉已更新後,您就可以新增選單選項,並將這些選項連接到剛剛新增的輔助方法。請更新 _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 顯示「Local demo page」的頁面標題以藍色顯示

Android 模擬器執行 Flutter 應用程式的螢幕截圖,其中內嵌 WebView 顯示「Local demo page」的頁面標題為黑色

13. 全部完成!

恭喜!您已完成程式碼研究室。您可以在程式碼研究室存放區中找到本程式碼研究室完成後的程式碼。

詳情請參閱其他 Flutter 程式碼研究室