Flutter アプリに WebView を追加する

1. はじめに

最終更新日: 2021 年 10 月 19 日

WebView Flutter プラグインを使用すると、Android または iOS の Flutter アプリに WebView ウィジェットを追加できます。iOS の場合、WebView ウィジェットは WKWebView を基盤としており、Android の場合、WebView ウィジェットは WebView を基盤としています。このプラグインは、ウェブビュー上に Flutter ウィジェットをレンダリングできます。たとえば、ウェブビュー上にプルダウン メニューを表示できます。

作成するアプリの概要

この Codelab では、Flutter SDK を使用し、WebView を備えたモバイルアプリを手順を追って作成します。作成するアプリの機能は次のとおりです。

  • ウェブ コンテンツを WebView に表示する
  • WebView の上に重ねて Flutter ウィジェットを表示する
  • ページ読み込みの進行イベントに反応する
  • WebViewController を通して WebView を制御する
  • NavigationDelegate を使用してウェブサイトをブロックする
  • JavaScript の式を評価する
  • JavascriptChannels を使用して JavaScript からのコールバックを処理する
  • Cookie を設定、削除、追加、表示する
  • HTML を含むアセット、ファイル、または文字列から HTML を読み込んで表示する

学習内容

この Codelab では、以下を含むさまざまな方法で webview_flutter プラグインを使用する方法を学びます。

  • webview_flutter プラグインを構成する方法
  • ページ読み込みの進行イベントをリッスンする方法
  • ページ ナビゲーションを制御する方法
  • 履歴内を前後に移動するよう WebView に指示する方法
  • JavaScript を評価し、返される結果を使用する方法
  • JavaScript から Dart コードを呼び出すコールバックを登録する方法
  • Cookie を管理する方法
  • HTML を含むアセット、ファイル、または文字列から HTML ページを読み込んで表示する方法

必要なもの

2. Flutter 環境をセットアップする

このラボを完了するには、Flutter SDKエディタの 2 つのソフトウェアが必要です。

この Codelab は、次のデバイスのどれを使用しても実行できます。

  • パソコンに接続され、デベロッパー モードに設定されているモバイル デバイスの実機(Android または iOS)
  • iOS シミュレータ(macOS のみ。Xcode ツールのインストールが必要)
  • Android Emulator(Android Studio でのセットアップが必要)

3. 始める

Flutter の使用を開始する

新しい Flutter プロジェクトを作成するには多くの方法があり、Android StudioVisual Studio Code の両方に、このタスク用のツールが用意されています。リンクされている手順に沿ってプロジェクトを作成するか、お手元のコマンドライン ターミナルで次のコマンドを実行します。

$ flutter create webview_in_flutter
Creating project webview_in_flutter...
[Listing of created files elided]
Wrote 81 files.

All done!
In order to run your application, type:

  $ cd webview_in_flutter
  $ flutter run

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

WebView Flutter プラグインを依存関係として追加する

Flutter アプリに機能を追加するには、Pub パッケージを使用すると簡単です。この Codelab では、webview_flutter プラグインをプロジェクトに追加します。ターミナルで次のコマンドを実行します。

$ cd webview_in_flutter
$ flutter pub add webview_flutter
Resolving dependencies...
  async 2.8.1 (2.8.2 available)
  characters 1.1.0 (1.2.0 available)
  matcher 0.12.10 (0.12.11 available)
+ plugin_platform_interface 2.0.2
  test_api 0.4.2 (0.4.8 available)
  vector_math 2.1.0 (2.1.1 available)
+ webview_flutter 3.0.0
+ webview_flutter_android 2.8.0
+ webview_flutter_platform_interface 1.8.0
+ webview_flutter_wkwebview 2.7.0
Downloading webview_flutter 3.0.0...
Downloading webview_flutter_wkwebview 2.7.0...
Downloading webview_flutter_android 2.8.0...
Changed 5 dependencies!

pubspec.yaml を調べると、webview_flutter プラグインの依存関係セクションに行があることがわかります。

Android minSDK を構成する

Android で webview_flutter プラグインを使用するには、使用する Android プラットフォームのビューに応じて、minSDK を 19 または 20 に設定する必要があります。Android プラットフォームのビューについて詳しくは、webview_flutter プラグインのページをご覧ください。android/app/build.gradle ファイルを次のように変更します。

android/app/build.gradle

defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.webview_in_flutter"
    minSdkVersion 20        // MODIFY
    targetSdkVersion 30
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

4.Flutter アプリに WebView ウィジェットを追加する

このステップでは、アプリに WebView を追加します。WebView はホスト型のネイティブ ビューです。アプリ デベロッパーは、このネイティブ ビューをアプリ内でホストする方法を選択できます。Android では、現在 Android のデフォルトとなっている仮想ディスプレイか、ハイブリッド コンポジションを選択できます。iOS では常にハイブリッド コンポジションが使用されます。

仮想ディスプレイとハイブリッド コンポジションの違いについて詳しくは、プラットフォーム ビューを使用して 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(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: 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 WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

iOS または Android でこれを実行すると、WebView がブラウザ ウィンドウとしてデバイスの画面全体に表示され、枠線や余白などは表示されません。スクロールすると、ページの一部が少し不自然に見えることがあります。これは、JavaScript が現在無効になっているからです。flutter.dev を適切にレンダリングするには JavaScript が必要です。

ハイブリッド コンポジションを有効にする

Android デバイスでハイブリッド コンポジション モードを使用する場合は、いくつかの簡単な変更を行います。lib/main.dart を次のように変更します。

lib/main.dart

import 'dart:io';                            // Add this import.
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

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

class _WebViewAppState extends State<WebViewApp> {
  // Add from here ...
  @override
  void initState() {
    if (Platform.isAndroid) {
      WebView.platform = SurfaceAndroidWebView();
    }
    super.initState();
  }
  // ... to here.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter WebView'),
      ),
      body: const WebView(
        initialUrl: 'https://flutter.dev',
      ),
    );
  }
}

ハイブリッド コンポジションのプラットフォーム ビューを使用する場合は、build.gradleminSdkVersion を 19 に変更します。

アプリを実行する

iOS または Android で Flutter アプリを実行すると、flutter.dev ウェブサイトを表示する WebView が表示されます。アプリは Android Emulator や iOS シミュレータで実行することもできます。WebView の最初の URL は、ご自分のウェブサイトなどに置き換えてもかまいません。

$ flutter run

適切なシミュレータまたはエミュレータを実行しているか、実機を接続していれば、アプリをコンパイルしてデバイスにデプロイすると、次のような表示を確認できます。

5. ページ読み込みイベントをリッスンする

WebView ウィジェットには、アプリがリッスンできるページ読み込みの進行イベントがいくつか用意されています。WebView のページ読み込みサイクル中には、onPageStartedonProgressonPageFinished の 3 種類のページ読み込みイベントが発生します。このステップでは、ページ読み込みインジケーターを実装します。追加要素として、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({Key? key}) : super(key: key);

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

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

このコードでは、WebView ウィジェットを Stack でラップし、ページ読み込みの割合が 100% 未満という条件で WebViewLinearProgressIndicator に重ねて表示しています。これには、時間とともに変化するプログラムの状態が関わるため、この状態を StatefulWidget に関連付けられた State クラスに保存しています。

この新しい WebViewStack ウィジェットを使用するには、lib/main.dart を次のように変更します。

import 'package:flutter/material.dart';
// Delete the package:webview_flutter/webview_flutter.dart import
import 'src/web_view_stack.dart';  // Add this import

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: 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(),   // Replace the WebView widget with 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 'dart:async';     // Add this import for Completer
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class WebViewStack extends StatefulWidget {
  const WebViewStack({required this.controller, Key? key}) : super(key: key); // Modify

  final Completer<WebViewController> controller;   // Add this attribute

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

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          // Add from here ...
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          // ... to here.
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

WebViewStack ウィジェットにより、Completer<WebViewController> を使用して非同期で作成されるコントローラが公開されるようになりました。この方法は、アプリの他の部分にコントローラを提供するコールバック関数の引数を作成するよりも軽量です。

ナビゲーション コントロールを作成する

WebView を機能させることは重要ですが、ページ履歴を前後に移動してページを再読み込みできればさらに便利になります。幸いなことに、WebViewController を使用してこの機能をアプリに追加できます。

lib/src/navigation_controls.dart に新しいソースファイルを作成し、次のコードを入力します。

lib/src/navigation_controls.dart

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

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

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, snapshot) {
        final WebViewController? controller = snapshot.data;
        if (snapshot.connectionState != ConnectionState.done ||
            controller == null) {
          return Row(
            children: const <Widget>[
              Icon(Icons.arrow_back_ios),
              Icon(Icons.arrow_forward_ios),
              Icon(Icons.replay),
            ],
          );
        }

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

このウィジェットは、コントローラが使用可能になったときに FutureBuilder<T> ウィジェットを使用して適切に再描画します。コントローラが使用可能になるまで待機している間、3 つのアイコンの行がレンダリングされます。コントローラが表示されると、controller を使用して機能を実装する、onPressed ハンドラを持つ IconButtonRow に置き換えられます。

AppBar にナビゲーション コントロールを追加する

更新した WebViewStack と新しく作成した NavigationControls を使用して、WebViewApp を更新し、すべてをまとめます。さきほど Completer<T> の使用方法を確認しましたが、作成先は確認しませんでした。このアプリのウィジェット ツリーの最上部に WebViewApp があるため、このレベルで作成することをおすすめします。

lib/main.dart ファイルを次のように更新します。

lib/main.dart

import 'dart:async';                                    // Add this import

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';  // Add this import back

import 'src/navigation_controls.dart';                  // Add this import
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

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

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();    // Instantiate the controller

  @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),       // Add the controller argument
    );
  }
}

アプリを実行すると、コントロールを含むウェブページが表示されます。

7. NavigationDelegate を使用してナビゲーションを追跡する

WebView はアプリに NavigationDelegate, を提供し、アプリが WebView ウィジェットのページ ナビゲーションを追跡、制御できるようにします。WebView, でナビゲーションが開始されると、たとえばユーザーがリンクをクリックすると、NavigationDelegate が呼び出されます。NavigationDelegate コールバックを使用すると、WebView がナビゲーションに進むかどうかを制御できます。

カスタムの NavigationDelegate を登録する

このステップでは、NavigationDelegate コールバックを登録して YouTube.com へのナビゲーションをブロックします。このシンプルな実装により、Flutter API のドキュメントのページに表示されるインラインの YouTube コンテンツもブロックされる点に注意してください。

lib/src/web_view_stack.dart を次のように更新します。

lib/src/web_view_stack.dart

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

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

  final Completer<WebViewController> controller;

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

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          // Add from here ...
          navigationDelegate: (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.
        ),
        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 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

enum _MenuOptions {
  navigationDelegate,
}

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

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
          ],
        );
      },
    );
  }
}

ユーザーが [Navigate to YouTube] メニュー オプションを選択すると、WebViewControllerloadUrl メソッドが実行されます。このナビゲーションは、前のステップで作成した navigationDelegate コールバックによってブロックされます。

WebViewApp の画面にメニューを追加するには、lib/main.dart を次のように変更します。

lib/main.dart

import 'dart:async';

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

import 'src/menu.dart';                                // Add this import
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';

void main() {
  runApp(
    const MaterialApp(
      home: WebViewApp(),
    ),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({Key? key}) : super(key: key);

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

class _WebViewAppState extends State<WebViewApp> {
  final controller = Completer<WebViewController>();

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

アプリを実行し、[Navigate to YouTube] メニュー項目をタップします。ナビゲーション コントローラが YouTube への移動をブロックしたことを示すスナックバーが表示されます。

9. JavaScript を評価する

WebViewController では、現在のページのコンテキストで JavaScript 式を評価できます。JavaScript を評価する方法は 2 つあります。値を返さない JavaScript コードには runJavaScript を使用し、値を返す JavaScript コードには runJavaScriptReturningResult を使用します。

JavaScript を有効にするには、WebView ウィジェットで javaScriptMode プロパティを JavascriptMode.unrestricted に設定する必要があります。javascriptMode はデフォルトで JavascriptMode.disabled に設定されます。

次のように javascriptMode 設定を追加して、_WebViewStackState クラスを更新します。

lib/src/web_view_stack.dart

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (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;
          },
          javascriptMode: JavascriptMode.unrestricted,        // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }
}

WebView で JavaScript を実行できるようになったため、runJavaScriptReturningResult メソッドを使用するオプションをメニューに追加できます。

lib/src/menu.dart を次のように変更します。

lib/src/menu.dart

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

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

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              // Add from here ...
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // ... to here.
            }
          },
          itemBuilder: (context) => [
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.navigationDelegate,
              child: Text('Navigate to YouTube'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.userAgent,
              child: Text('Show user-agent'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

[Show user-agent] メニュー オプションをタップすると、JavaScript の式 navigator.userAgent の実行結果が Snackbar に表示されます。アプリを実行すると、Flutter.dev ページの表示が異なることがわかります。これが JavaScript を有効にして実行した結果です。

10. JavaScript チャネルを使用する

JavascriptChannel により、アプリで WebView の JavaScript コンテキストにコールバック ハンドラを登録できるようになります。これを呼び出して、アプリの Dart コードに値を返せます。このステップでは、XMLHttpRequest の結果とともに呼び出す SnackBar チャネルを登録します。

WebViewStack クラスを次のように更新します。

lib/src/web_view_stack.dart

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

  final Completer<WebViewController> controller;

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

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

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        WebView(
          initialUrl: 'https://flutter.dev',
          onWebViewCreated: (webViewController) {
            widget.controller.complete(webViewController);
          },
          onPageStarted: (url) {
            setState(() {
              loadingPercentage = 0;
            });
          },
          onProgress: (progress) {
            setState(() {
              loadingPercentage = progress;
            });
          },
          onPageFinished: (url) {
            setState(() {
              loadingPercentage = 100;
            });
          },
          navigationDelegate: (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;
          },
          javascriptMode: JavascriptMode.unrestricted,
          javascriptChannels: _createJavascriptChannels(context),  // Add this line
        ),
        if (loadingPercentage < 100)
          LinearProgressIndicator(
            value: loadingPercentage / 100.0,
          ),
      ],
    );
  }

  // Add from here ...
  Set<JavascriptChannel> _createJavascriptChannels(BuildContext context) {
    return {
      JavascriptChannel(
        name: 'SnackBar',
        onMessageReceived: (message) {
          ScaffoldMessenger.of(context)
              .showSnackBar(SnackBar(content: Text(message.message)));
        },
      ),
    };
  }
  // ... to here.
}

Set 内の JavascriptChannel ごとに、JavaScript のコンテキストで、JavascriptChannel.name と同じ名前のウィンドウ プロパティとして、チャネル オブジェクトが使用できるようになります。これを JavaScript コンテキストから使用するには、JavascriptChannel に対して postMessage を呼び出し、指定の JavascriptChannelonMessageReceived コールバック ハンドラに渡すメッセージを送信します。

上記で追加した JavascriptChannel を使用するには、JavaScript コンテキストで XMLHttpRequest を実行し、SnackBar JavascriptChannel を使用して結果を返す別のメニュー項目を追加します。

WebViewJavascriptChannels, が認識されたので、アプリをさらに拡張するサンプルを追加します。これを行うには、Menu クラスに PopupMenuItem を追加して機能を追加します。

javascriptChannel 列挙値を追加して、追加のメニュー オプションで _MenuOptions を更新し、次のように Menu クラスに実装を追加します。

lib/src/menu.dart

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

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

  final Completer<WebViewController> controller;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              // Add from here ...
              case _MenuOptions.javascriptChannel:
                await controller.data!.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();''');
                break;
              // ... 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'),
            ),
            // Add from here ...
            const PopupMenuItem<_MenuOptions>(
              value: _MenuOptions.javascriptChannel,
              child: Text('Lookup IP Address'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }
}

ユーザーが [JavaScript Channel Example] メニュー オプションを選択すると、この JavaScript が実行されます。

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

このコードは、GET リクエストを Public 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 クラスに焦点を当てています。MenuCookieManager を所有する必要があり、ステートレス ウィジェットでの変更可能な状態は不適切な組み合わせであるため、この変更は重要です。

エディタまたはキーボードを使用して、次のように Menu クラスを StatefulWidget に変換し、CookieManager を結果の State クラスに追加します。

lib/src/menu.dart

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

  final Completer<WebViewController> controller;

  @override
  State<Menu> createState() => _MenuState();                  // Add this line
}

class _MenuState extends State<Menu> {                       // New State class
  final CookieManager cookieManager = CookieManager();       // Add this line

  @override
  Widget build(BuildContext context) {
  // ...

_MenuState クラスには、以前に Menu クラスに追加したコードと、新しく追加された CookieManager が含まれます。この後の一連のセクションでは、これから追加するメニュー項目から呼び出すヘルパー関数を _MenuState に追加します。

すべての Cookie のリストを取得する

すべての Cookie のリストを取得するには、JavaScript を使用します。そのためには、_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');
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
    ),
  );
}

Cookie をすべて消去する

WebView のすべての Cookie を消去するには、CookieManager クラスの clearCookies メソッドを使用します。このメソッドは Future<bool> を返します。その値は、CookieManager が Cookie を消去した場合は 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.';
  }
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text(message),
    ),
  );
}

Cookie を追加するには、JavaScript を呼び出します。JavaScript ドキュメントに Cookie を追加するために使用する API については、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();''');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie added.'),
    ),
  );
}

Cookie は、以下のように CookieManager を使用して設定することもできます。

_MenuState クラスに以下を追加します。

lib/src/menu.dart

Future<void> _onSetCookie(WebViewController controller) async {
  await cookieManager.setCookie(
    const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
  );
  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" ');
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Custom cookie removed.'),
    ),
  );
}

CookieManager のメニュー項目を追加する

あとは、メニュー オプションを追加し、先ほど追加したヘルパー メソッドに接続するだけです。_MenuState クラスを次のように更新します。

lib/src/menu.dart

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>(
      future: widget.controller.future,
      builder: (context, controller) {
        return PopupMenuButton<_MenuOptions>(
          onSelected: (value) async {
            switch (value) {
              case _MenuOptions.navigationDelegate:
                controller.data!.loadUrl('https://youtube.com');
                break;
              case _MenuOptions.userAgent:
                final userAgent = await controller.data!
                    .runJavascriptReturningResult('navigator.userAgent');
                ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                  content: Text(userAgent),
                ));
                break;
              case _MenuOptions.javascriptChannel:
                await controller.data!.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();''');
                break;
              // Add from here ...
              case _MenuOptions.clearCookies:
                _onClearCookies();
                break;
              case _MenuOptions.listCookies:
                _onListCookies(controller.data!);
                break;
              case _MenuOptions.addCookie:
                _onAddCookie(controller.data!);
                break;
              case _MenuOptions.setCookie:
                _onSetCookie(controller.data!);
                break;
              case _MenuOptions.removeCookie:
                _onRemoveCookie(controller.data!);
                break;
              // ... 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'),
            ),
            // Add from here ...
            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'),
            ),
            // ... to here.
          ],
        );
      },
    );
  }

CookieManager を使ってみる

アプリに追加したすべての機能を使用するには、次の手順をお試しください。

  1. [List cookies] を選択します。flutter.dev で設定された Google アナリティクスの Cookie リストが表示されます。
  2. [Clear cookies] を選択します。Cookie が消去されたというメッセージが表示されます。
  3. もう一度 [Clear cookies] を選択します。消去できる Cookie がないというメッセージが表示されます。
  4. [List cookies] を選択します。Cookie がないというメッセージが表示されます。
  5. [Add cookie] を選択します。Cookie が追加されたというメッセージが表示されます。
  6. [Set cookie] を選択します。Cookie が設定されたというメッセージが表示されます。
  7. [List cookies] を選択し、最後に [Remove cookie] を選択します。

12. WebView で Flutter のアセット、ファイル、HTML 文字列を読み込む

アプリでは、さまざまな方法で HTML ファイルを読み込み、WebView に表示できます。このステップでは、pubspec.yaml ファイルで指定された Flutter アセットの読み込み、指定するパスにあるファイルの読み込み、HTML 文字列を使用したページの読み込みを行います。

指定するパスにあるファイルを読み込むには、pubspec.yamlpath_provider を追加する必要があります。これは、ファイル システムでよく使用されている場所を見つけるための Flutter プラグインです。

pubspec.yaml に次の行を追加します。

pubspec.yaml

dependencies:
 flutter:
   sdk: flutter

 # The following adds the Cupertino Icons font to your application.
 # Use with the CupertinoIcons class for iOS style icons.
 cupertino_icons: ^1.0.2
 webview_flutter: ^3.0.0
 path_provider: ^2.0.7   # Add this line

アセットを読み込むには、pubspec.yaml 内でアセットへのパスを指定する必要があります。pubspec.yaml に次の行を追加します。

pubspec.yaml

# The following section is specific to Flutter.
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. 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 メソッドを使用するメソッドを追加します。この場合も、ファイルのパスを含む String を受け取る WebViewController を使用します。

まず、HTML コードを含むファイルを作成する必要があります。これを行うには、menu.dart ファイルで、一連の import のすぐ下に、HTML コードを文字列として追加します。

lib/src/menu.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
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> _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;
}
// ... to here.

HTML 文字列を読み込む

HTML 文字列を指定してページを表示するのは、非常に簡単です。WebViewController には、HTML 文字列を引数として指定できる loadHtmlString というメソッドがあります。この場合、指定する HTML ページが WebView に表示されます。次のメソッドをコードに追加します。

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 cookieManager = CookieManager();

 @override
 Widget build(BuildContext context) {
   return FutureBuilder<WebViewController>(
     future: widget.controller.future,
     builder: (context, controller) {
       return PopupMenuButton<_MenuOptions>(
         onSelected: (value) async {
           switch (value) {
             case _MenuOptions.navigationDelegate:
               controller.data!.loadUrl('https://youtube.com');
               break;
             case _MenuOptions.userAgent:
               final userAgent = await controller.data!
                   .runJavascriptReturningResult('navigator.userAgent');
               ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                 content: Text(userAgent),
               ));
               break;
             case _MenuOptions.javascriptChannel:
               await controller.data!.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();''');
               break;
             case _MenuOptions.clearCookies:
               _onClearCookies();
               break;
             case _MenuOptions.listCookies:
               _onListCookies(controller.data!);
               break;
             case _MenuOptions.addCookie:
               _onAddCookie(controller.data!);
               break;
             case _MenuOptions.setCookie:
               _onSetCookie(controller.data!);
               break;
             case _MenuOptions.removeCookie:
               _onRemoveCookie(controller.data!);
               Break;
             // Add from here ...
             case _MenuOptions.loadFlutterAsset:
               _onLoadFlutterAssetExample(controller.data!, context);
               break;
             case _MenuOptions.loadLocalFile:
               _onLoadLocalFileExample(controller.data!, context);
               break;
             case _MenuOptions.loadHtmlString:
               _onLoadHtmlStringExample(controller.data!, context);
               Break;
             // ... 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'),
           ),
           // Add from here ...
           const PopupMenuItem<_MenuOptions>(
             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 ファイルのヘッダーを青色に変更している点に注目してください。

13. 完了

お疲れさまでした。Codelab を完了しました。この Codelab の完全なコードは、Codelab リポジトリにあります。

詳細については、別の Flutter の Codelab をご覧ください。