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 ページを読み込んで表示する方法
必要なもの
- Android Studio 4.1 以降(Android 開発用)
- Xcode 12 以降(iOS 開発用)
- Flutter SDK
- Android Studio、Visual Studio Code、Emacs などのコードエディタ
2. Flutter 環境をセットアップする
このラボを完了するには、Flutter SDK とエディタの 2 つのソフトウェアが必要です。
この Codelab は、次のデバイスのどれを使用しても実行できます。
- パソコンに接続され、デベロッパー モードに設定されているモバイル デバイスの実機(Android または iOS)
- iOS シミュレータ(macOS のみ。Xcode ツールのインストールが必要)
- Android Emulator(Android Studio でのセットアップが必要)
3. 始める
Flutter の使用を開始する
新しい Flutter プロジェクトを作成するには多くの方法があり、Android Studio と Visual 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.gradle
の minSdkVersion
を 19 に変更します。
アプリを実行する
iOS または Android で Flutter アプリを実行すると、flutter.dev ウェブサイトを表示する WebView が表示されます。アプリは Android Emulator や iOS シミュレータで実行することもできます。WebView の最初の URL は、ご自分のウェブサイトなどに置き換えてもかまいません。
$ flutter run
適切なシミュレータまたはエミュレータを実行しているか、実機を接続していれば、アプリをコンパイルしてデバイスにデプロイすると、次のような表示を確認できます。
5. ページ読み込みイベントをリッスンする
WebView
ウィジェットには、アプリがリッスンできるページ読み込みの進行イベントがいくつか用意されています。WebView
のページ読み込みサイクル中には、onPageStarted
、onProgress
、onPageFinished
の 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% 未満という条件で WebView
を LinearProgressIndicator
に重ねて表示しています。これには、時間とともに変化するプログラムの状態が関わるため、この状態を 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
ハンドラを持つ IconButton
の Row
に置き換えられます。
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] メニュー オプションを選択すると、WebViewController
の loadUrl
メソッドが実行されます。このナビゲーションは、前のステップで作成した 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
を呼び出し、指定の JavascriptChannel
の onMessageReceived
コールバック ハンドラに渡すメッセージを送信します。
上記で追加した JavascriptChannel
を使用するには、JavaScript コンテキストで XMLHttpRequest
を実行し、SnackBar
JavascriptChannel
を使用して結果を返す別のメニュー項目を追加します。
WebView
で JavascriptChannels,
が認識されたので、アプリをさらに拡張するサンプルを追加します。これを行うには、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
クラスに焦点を当てています。Menu
は CookieManager
を所有する必要があり、ステートレス ウィジェットでの変更可能な状態は不適切な組み合わせであるため、この変更は重要です。
エディタまたはキーボードを使用して、次のように 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 を追加する
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.'),
),
);
}
CookieManager を使用して Cookie を設定する
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 を削除するには、有効期限を過去の日付に設定した 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 を使ってみる
アプリに追加したすべての機能を使用するには、次の手順をお試しください。
- [List cookies] を選択します。flutter.dev で設定された Google アナリティクスの Cookie リストが表示されます。
- [Clear cookies] を選択します。Cookie が消去されたというメッセージが表示されます。
- もう一度 [Clear cookies] を選択します。消去できる Cookie がないというメッセージが表示されます。
- [List cookies] を選択します。Cookie がないというメッセージが表示されます。
- [Add cookie] を選択します。Cookie が追加されたというメッセージが表示されます。
- [Set cookie] を選択します。Cookie が設定されたというメッセージが表示されます。
- [List cookies] を選択し、最後に [Remove cookie] を選択します。
12. WebView で Flutter のアセット、ファイル、HTML 文字列を読み込む
アプリでは、さまざまな方法で HTML ファイルを読み込み、WebView に表示できます。このステップでは、pubspec.yaml
ファイルで指定された Flutter アセットの読み込み、指定するパスにあるファイルの読み込み、HTML 文字列を使用したページの読み込みを行います。
指定するパスにあるファイルを読み込むには、pubspec.yaml
に path_provider
を追加する必要があります。これは、ファイル システムでよく使用されている場所を見つけるための Flutter プラグインです。
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.
アセットをプロジェクトに追加する手順は次のとおりです。
- プロジェクトのルートフォルダに、
assets
という名前の新しいディレクトリを作成します。 assets
フォルダに、www
という名前の新しいディレクトリを作成します。www
フォルダに、styles
という名前の新しいディレクトリを作成します。www
フォルダに、index.html
という名前の新しいファイルを作成します。styles
フォルダに、style.css
という名前の新しいファイルを作成します。
次のコードをコピーして 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 ヘッダーのスタイルを設定します。
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 コードを文字列として追加します。
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()
メソッドで返される文字列としてパスを指定して、ファイルを読み込みます。次のメソッドをコードに追加します。
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
に表示されます。次のメソッドをコードに追加します。
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 をご覧ください。