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 シミュレータ(Xcode ツールのインストールが必要)
  • Android Emulator(Android Studio でセットアップが必要)

3. 開始するには

Flutter の使用を開始する

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

$ flutter create --platforms=android,ios webview_in_flutter
Creating project webview_in_flutter...
Running "flutter pub get" in webview_in_flutter...               1,728ms
Wrote 73 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

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

Android minSDK を構成する

Android で webview_flutter プラグインを使用するには、minSDK20 に設定する必要があります。android/app/build.gradle ファイルを次のように変更します。

android/app/build.gradle

android {
    //...

    defaultConfig {
        applicationId "com.example.webview_in_flutter"
        minSdkVersion 20                           // MODIFY
        targetSdkVersion flutter.targetSdkVersion
        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(
      theme: ThemeData(useMaterial3: true),
      home: 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 が必要です。

アプリを実行する

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({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(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: 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(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: 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
    );
  }
}

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

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 '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'));
            break;
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
      ],
    );
  }
}

ユーザーが [Navigate to 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(
    const MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: 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] メニュー項目をタップします。ナビゲーション コントローラが YouTube への移動をブロックしたことを示すスナックバーが表示されます。

9. JavaScript を評価する

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

JavaScript を有効にするには、WebViewControllerjavaScriptMode プロパティを JavascriptMode.unrestricted に構成する必要があります。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(
        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);
  }

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

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

エディタまたはキーボードを使用して、Menu クラスを 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,
}

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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
        }
      },
      itemBuilder: (context) => [
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.navigationDelegate,
          child: Text('Navigate to YouTube'),
        ),
        const PopupMenuItem<_MenuOptions>(
          value: _MenuOptions.userAgent,
          child: Text('Show user-agent'),
        ),
      ],
    );
  }
}

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

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 チャネルが認識できるようになったので、コード例を追加してアプリをさらに拡張します。これを行うには、Menu クラスに PopupMenuItem を追加して機能を追加します。

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

lib/src/menu.dart

enum _MenuOptions {
  navigationDelegate,
  userAgent,
  javascriptChannel,
}

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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          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();''');
            break;
        }
      },
      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'),
        ),
      ],
    );
  }
}

ユーザーが [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 を所有する必要があり、ステートレス ウィジェットでの変更可能な状態は不適切な組み合わせであるため、この変更は重要です。

次のように、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 のリストを取得する

すべての 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') as String;
  if (!mounted) return;
  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.';
  }
  if (!mounted) return;
  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();''');
  if (!mounted) return;
  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'),
  );
  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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          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();''');
            break;
          case _MenuOptions.clearCookies:
            await _onClearCookies();
            break;
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
            break;
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
            break;
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
            break;
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
            break;
        }
      },
      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'),
        ),
      ],
    );
  }

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 プラグインです。

コマンドラインで、次のコマンドを実行します。

$ flutter pub add path_provider

アセットを読み込むには、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: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 には、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 = 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'));
            break;
          case _MenuOptions.userAgent:
            final userAgent = await widget.controller
                .runJavaScriptReturningResult('navigator.userAgent');
            if (!mounted) return;
            ScaffoldMessenger.of(context).showSnackBar(SnackBar(
              content: Text('$userAgent'),
            ));
            break;
          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();''');
            break;
          case _MenuOptions.clearCookies:
            await _onClearCookies();
            break;
          case _MenuOptions.listCookies:
            await _onListCookies(widget.controller);
            break;
          case _MenuOptions.addCookie:
            await _onAddCookie(widget.controller);
            break;
          case _MenuOptions.setCookie:
            await _onSetCookie(widget.controller);
            break;
          case _MenuOptions.removeCookie:
            await _onRemoveCookie(widget.controller);
            break;
          case _MenuOptions.loadFlutterAsset:
            await _onLoadFlutterAssetExample(widget.controller, context);
            break;
          case _MenuOptions.loadLocalFile:
            await _onLoadLocalFileExample(widget.controller, context);
            break;
          case _MenuOptions.loadHtmlString:
            await _onLoadHtmlStringExample(widget.controller, context);
            break;
        }
      },
      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>(
          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'),
        ),
      ],
    );
  }

アセット、ファイル、HTML 文字列をテストする

実装したコードが正しく機能するかどうかをテストするには、デバイスでコードを実行し、新しく追加したメニュー項目のいずれかをクリックします。_onLoadFlutterAssetExample では、追加した style.css を使用して HTML ファイルのヘッダーを青色に変更している点に注目してください。

13. 完了

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

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