1. 簡介
上次更新時間:2021 年 10 月 19 日
有了 WebView Flutter 外掛程式,您就能在 Android 或 iOS Flutter 應用程式中新增 WebView 小工具。iOS 版 WebView 小工具由 WKWebView 支援,而 Android 版 WebView 小工具則由 WebView 支援。這個外掛程式可在網頁檢視畫面中算繪 Flutter 小工具。例如,您可以在網頁檢視畫面中顯示下拉式選單。
建構項目
在本程式碼研究室中,您將使用 Flutter SDK 逐步建構 WebView。您的應用程式將會:
- 在
WebView
中顯示網路內容 - 顯示堆疊在
WebView
上的 Flutter 小工具 - 回應網頁載入進度事件
- 透過
WebViewController
控制WebView
- 使用
NavigationDelegate
封鎖網站 - 評估 JavaScript 運算式
- 使用
JavascriptChannels
處理 JavaScript 的回呼 - 設定、移除、新增或顯示 Cookie
- 從含有 HTML 的素材資源、檔案或字串中載入並顯示 HTML
課程內容
在本程式碼研究室中,您將瞭解如何透過多種方式使用 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 和編輯器。
您可以使用下列任一裝置執行程式碼研究室:
- 將實體 Android 或 iOS 裝置接上電腦,並設為開發人員模式。
- iOS 模擬器 (需要安裝 Xcode 工具)。
- Android Emulator (需要在 Android Studio 中設定)。
3. 開始使用
開始使用 Flutter
建立新 Flutter 專案的方式有很多種,Android Studio 和 Visual Studio Code 都提供了這項工作所需的工具。請按照連結的程序建立專案,或是在便利的指令列終端機中執行下列指令。
$ flutter create --platforms=android,ios webview_in_flutter Creating project webview_in_flutter... Resolving dependencies in `webview_in_flutter`... Downloading packages... Got dependencies in `webview_in_flutter`. Wrote 74 files. All done! You can find general documentation for Flutter at: https://docs.flutter.dev/ Detailed API documentation is available at: https://api.flutter.dev/ If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev In order to run your application, type: $ cd webview_in_flutter $ flutter run Your application code is in webview_in_flutter/lib/main.dart.
將 WebView Flutter 外掛程式新增為依附元件
使用 Pub 套件輕鬆為 Flutter 應用程式新增額外功能。在這個程式碼研究室中,您要在專案中新增 webview_flutter
外掛程式。在終端機中執行下列指令。
$ cd webview_in_flutter $ flutter pub add webview_flutter Resolving dependencies... Downloading packages... leak_tracker 10.0.4 (10.0.5 available) leak_tracker_flutter_testing 3.0.3 (3.0.5 available) material_color_utilities 0.8.0 (0.11.1 available) meta 1.12.0 (1.14.0 available) + plugin_platform_interface 2.1.8 test_api 0.7.0 (0.7.1 available) + webview_flutter 4.7.0 + webview_flutter_android 3.16.0 + webview_flutter_platform_interface 2.10.0 + webview_flutter_wkwebview 3.13.0 Changed 5 dependencies! 5 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
如果您檢查 pubspec.yaml,您會發現 webview_flutter
外掛程式的依附元件區段中有一行文字。
設定 Android minSDK
如要在 Android 上使用 webview_flutter
外掛程式,您必須將 minSDK
設為 20
。按照下列方式修改 android/app/build.gradle
檔案:
android/app/build.gradle
android {
//...
defaultConfig {
applicationId = "com.example.webview_in_flutter"
minSdk = 20 // Modify this line
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
4. 在 Flutter 應用程式中加入 WebView 小工具
在這個步驟中,您將在應用程式中新增 WebView
。WebView 是代管式的原生檢視畫面,應用程式開發人員可以選擇如何在應用程式中代管這些原生檢視畫面。在 Android 上,你有選擇虛擬螢幕 (目前 Android 預設) 和混合組合。不過,iOS 一律使用混合型組合。
如要深入瞭解虛擬顯示器和混合型組合之間的差異,請參閱使用 Platform View 在 Flutter 應用程式中代管原生 Android 和 iOS 檢視畫面相關說明文件。
將 WebView 顯示在畫面上
將 lib/main.dart
改成以下內容:
lib/main.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const WebViewApp(),
),
);
}
class WebViewApp extends StatefulWidget {
const WebViewApp({super.key});
@override
State<WebViewApp> createState() => _WebViewAppState();
}
class _WebViewAppState extends State<WebViewApp> {
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..loadRequest(
Uri.parse('https://flutter.dev'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView'),
),
body: WebViewWidget(
controller: controller,
),
);
}
}
在 iOS 或 Android 裝置上執行 WebView 後,WebView 會在裝置上顯示為全出血的瀏覽器視窗,因此瀏覽器會以全螢幕模式顯示裝置,不會有任何邊框或邊界。捲動畫面時,你可能會發現網頁某些部分看起來有點奇怪。這是因為目前 JavaScript 已停用,必須啟用 JavaScript 才能正確轉譯 flutter.dev。
執行應用程式
在 iOS 或 Android 中執行 Flutter 應用程式,即可查看顯示 flutter.dev 網站的 WebView。或者,在 Android 模擬器或 iOS 模擬器中執行應用程式。您可以隨意將初始的 WebView 網址替換為您自己的網站。
$ flutter run
假設您已執行適當的模擬器或模擬器,或是已連接實體裝置,在編譯並部署應用程式後,畫面應會顯示如下的內容:
5. 監聽網頁載入事件
WebView
小工具提供多個網頁載入進度事件,供應用程式監聽。在 WebView
網頁載入循環中,會觸發三種不同的網頁載入事件:onPageStarted
、onProgress
和 onPageFinished
。在這個步驟中,您將實作網頁載入指標。此外,這會顯示您可以在 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% 時,有條件地將 WebView
與 LinearProgressIndicator
重疊。由於這涉及會隨著時間變更的程式狀態,所以您已將此狀態儲存在與 StatefulWidget
相關聯的 State
類別中。
如要使用這個新的 WebViewStack
小工具,請按照下列方式修改 lib/main.dart:
lib/main.dart
import 'package:flutter/material.dart';
import 'src/web_view_stack.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const WebViewApp(),
),
);
}
class WebViewApp extends StatefulWidget {
const WebViewApp({super.key});
@override
State<WebViewApp> createState() => _WebViewAppState();
}
class _WebViewAppState extends State<WebViewApp> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView'),
),
body: const WebViewStack(),
);
}
}
執行應用程式時,視網路狀況,以及瀏覽器是否已快取您要前往的頁面,您會在 WebView
內容區域上方看到網頁載入指標。
6. 使用 WebViewController
透過 WebView 小工具存取 WebViewController
WebView
小工具透過 WebViewController
提供程式輔助控制選項。這個控制器可在透過回呼建構 WebView
小工具後使用。這個控制器的可用性非同步性質,因此成為 Dart 非同步 Completer<T>
類別的最佳候選者。
依照下列方式更新 lib/src/web_view_stack.dart
:
lib/src/web_view_stack.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewStack extends StatefulWidget {
const WebViewStack({required this.controller, super.key}); // MODIFY
final WebViewController controller; // ADD
@override
State<WebViewStack> createState() => _WebViewStackState();
}
class _WebViewStackState extends State<WebViewStack> {
var loadingPercentage = 0;
// REMOVE the controller that was here
@override
void initState() {
super.initState();
// Modify from here...
widget.controller.setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
setState(() {
loadingPercentage = 0;
});
},
onProgress: (progress) {
setState(() {
loadingPercentage = progress;
});
},
onPageFinished: (url) {
setState(() {
loadingPercentage = 100;
});
},
),
);
// ...to here.
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(
controller: widget.controller, // MODIFY
),
if (loadingPercentage < 100)
LinearProgressIndicator(
value: loadingPercentage / 100.0,
),
],
);
}
}
WebViewStack
小工具現在會使用在周圍小工具中建立的控制器。這樣就能輕鬆與應用程式的其他部分共用 WebViewWidget
的控制器。
編寫導覽控制項
確保 WebView
正常運作是一回事,但如果能在網頁記錄中來回瀏覽,再重新載入網頁,是非常實用的做法。幸好,使用 WebViewController
即可在應用程式中加入這項功能。
在 lib/src/navigation_controls.dart
中建立新的來源檔案,並填入以下內容:
lib/src/navigation_controls.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class NavigationControls extends StatelessWidget {
const NavigationControls({required this.controller, super.key});
final WebViewController controller;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
if (await controller.canGoBack()) {
await controller.goBack();
} else {
messenger.showSnackBar(
const SnackBar(content: Text('No back history item')),
);
return;
}
},
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: () async {
final messenger = ScaffoldMessenger.of(context);
if (await controller.canGoForward()) {
await controller.goForward();
} else {
messenger.showSnackBar(
const SnackBar(content: Text('No forward history item')),
);
return;
}
},
),
IconButton(
icon: const Icon(Icons.replay),
onPressed: () {
controller.reload();
},
),
],
);
}
}
這個小工具會在建構期間使用與此小工具共用的 WebViewController
,讓使用者透過一系列的 IconButton
控制 WebView
。
在 AppBar 新增導覽控制項
隨著更新後的 WebViewStack
以及全新設計的 NavigationControls
一手掌握,現在您可以把它全部整合至新版 WebViewApp
中。我們會在此處建構共用的 WebViewController
。在這個應用程式中,將 WebViewApp
靠近小工具樹狀結構頂端的位置,是合理的做法。
依照下列方式更新 lib/main.dart
檔案:
lib/main.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart'; // ADD
import 'src/navigation_controls.dart'; // ADD
import 'src/web_view_stack.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const WebViewApp(),
),
);
}
class WebViewApp extends StatefulWidget {
const WebViewApp({super.key});
@override
State<WebViewApp> createState() => _WebViewAppState();
}
class _WebViewAppState extends State<WebViewApp> {
// Add from here...
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..loadRequest(
Uri.parse('https://flutter.dev'),
);
}
// ...to here.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView'),
// Add from here...
actions: [
NavigationControls(controller: controller),
],
// ...to here.
),
body: WebViewStack(controller: controller), // MODIFY
);
}
}
執行應用程式應會顯示含有控制項的網頁:
7. 使用 NavigationDelegate 追蹤導航
WebView
為應用程式提供 NavigationDelegate,
,可讓應用程式追蹤及控制 WebView
小工具的頁面導覽。當 WebView,
啟動導覽 (例如當使用者點選連結時),系統會呼叫 NavigationDelegate
。NavigationDelegate
回呼可用來控制 WebView
是否繼續導覽。
註冊自訂 NavigationDelegate
在這個步驟中,您將註冊 NavigationDelegate
回呼,封鎖前往 YouTube.com 的導覽功能。請注意,這項簡化的導入方式也會封鎖內嵌 YouTube 內容,這些內容會顯示在各種 Flutter API 說明文件頁面中。
依照下列方式更新 lib/src/web_view_stack.dart
:
lib/src/web_view_stack.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebViewStack extends StatefulWidget {
const WebViewStack({required this.controller, super.key});
final WebViewController controller;
@override
State<WebViewStack> createState() => _WebViewStackState();
}
class _WebViewStackState extends State<WebViewStack> {
var loadingPercentage = 0;
@override
void initState() {
super.initState();
widget.controller.setNavigationDelegate(
NavigationDelegate(
onPageStarted: (url) {
setState(() {
loadingPercentage = 0;
});
},
onProgress: (progress) {
setState(() {
loadingPercentage = progress;
});
},
onPageFinished: (url) {
setState(() {
loadingPercentage = 100;
});
},
// Add from here...
onNavigationRequest: (navigation) {
final host = Uri.parse(navigation.url).host;
if (host.contains('youtube.com')) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Blocking navigation to $host',
),
),
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
// ...to here.
),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(
controller: widget.controller,
),
if (loadingPercentage < 100)
LinearProgressIndicator(
value: loadingPercentage / 100.0,
),
],
);
}
}
在下一個步驟中,您將新增選單項目,以使用 WebViewController
類別測試 NavigationDelegate
。這可以作為練習者的練習,加強回呼的邏輯,僅封鎖前往 YouTube.com 的整頁導覽,同時仍允許 API 說明文件中內嵌 YouTube 內容。
8. 將選單按鈕新增至 AppBar
在接下來的幾個步驟中,您將在 AppBar
小工具中製作選單按鈕,用來評估 JavaScript、叫用 JavaScript 管道及管理 Cookie。總之,提供實用的菜單。
在 lib/src/menu.dart
中建立新的來源檔案,並填入以下內容:
lib/src/menu.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
enum _MenuOptions {
navigationDelegate,
}
class Menu extends StatelessWidget {
const Menu({required this.controller, super.key});
final WebViewController controller;
@override
Widget build(BuildContext context) {
return PopupMenuButton<_MenuOptions>(
onSelected: (value) async {
switch (value) {
case _MenuOptions.navigationDelegate:
await controller.loadRequest(Uri.parse('https://youtube.com'));
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
],
);
}
}
使用者選取「前往 YouTube」選單選項時,系統會執行 WebViewController
的 loadRequest
方法。您在上一個步驟中建立的 navigationDelegate
回呼會封鎖這項導覽。
如要將選單新增至 WebViewApp
的螢幕,請按照下列方式修改 lib/main.dart
:
lib/main.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'src/menu.dart'; // ADD
import 'src/navigation_controls.dart';
import 'src/web_view_stack.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData(useMaterial3: true),
home: const WebViewApp(),
),
);
}
class WebViewApp extends StatefulWidget {
const WebViewApp({super.key});
@override
State<WebViewApp> createState() => _WebViewAppState();
}
class _WebViewAppState extends State<WebViewApp> {
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..loadRequest(
Uri.parse('https://flutter.dev'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView'),
actions: [
NavigationControls(controller: controller),
Menu(controller: controller), // ADD
],
),
body: WebViewStack(controller: controller),
);
}
}
執行應用程式,然後輕觸「Navigate to YouTube」選單項目。系統應該會播放 SnackBar,通知您導覽控制器封鎖了導覽至 YouTube。
9. 評估 JavaScript
WebViewController
可以根據目前網頁的內容評估 JavaScript 運算式。評估 JavaScript 的方法有兩種:如果是未傳回值的 JavaScript 程式碼,請使用 runJavaScript
;如果是傳回值的 JavaScript 程式碼,請使用 runJavaScriptReturningResult
。
如要啟用 JavaScript,您必須在 javaScriptMode
屬性設為 JavascriptMode.unrestricted
的情況下設定 WebViewController
。根據預設,javascriptMode
設為 JavascriptMode.disabled
。
新增 javascriptMode
設定以更新 _WebViewStackState
類別,如下所示:
lib/src/web_view_stack.dart
class _WebViewStackState extends State<WebViewStack> {
var loadingPercentage = 0;
@override
void initState() {
super.initState();
widget.controller
..setNavigationDelegate( // Modify this line to use .. instead of .
NavigationDelegate(
onPageStarted: (url) {
setState(() {
loadingPercentage = 0;
});
},
onProgress: (progress) {
setState(() {
loadingPercentage = progress;
});
},
onPageFinished: (url) {
setState(() {
loadingPercentage = 100;
});
},
onNavigationRequest: (navigation) {
final host = Uri.parse(navigation.url).host;
if (host.contains('youtube.com')) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Blocking navigation to $host',
),
),
);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..setJavaScriptMode(JavaScriptMode.unrestricted); // Add this line
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(
controller: widget.controller,
),
if (loadingPercentage < 100)
LinearProgressIndicator(
value: loadingPercentage / 100.0,
),
],
);
}
}
現在 WebViewWidget
可以執行 JavaScript,您可以在選單中新增使用 runJavaScriptReturningResult
方法的選項。
使用編輯器或部分鍵盤,將選單類別轉換為 StatefulWidget。修改 lib/src/menu.dart
以使其符合以下內容:
lib/src/menu.dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
enum _MenuOptions {
navigationDelegate,
userAgent, // Add this line
}
class Menu extends StatefulWidget { // Convert to StatefulWidget
const Menu({required this.controller, super.key});
final WebViewController controller;
@override // Add from here
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> { // To here.
@override
Widget build(BuildContext context) {
return PopupMenuButton<_MenuOptions>(
onSelected: (value) async {
switch (value) {
case _MenuOptions.navigationDelegate: // Modify from here
await widget.controller
.loadRequest(Uri.parse('https://youtube.com'));
case _MenuOptions.userAgent:
final userAgent = await widget.controller
.runJavaScriptReturningResult('navigator.userAgent');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('$userAgent'),
)); // To here.
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
const PopupMenuItem<_MenuOptions>( // Add from here
value: _MenuOptions.userAgent,
child: Text('Show user-agent'),
), // To here.
],
);
}
}
輕觸 [顯示使用者代理程式]選單選項,執行 JavaScript 運算式 navigator.userAgent
的結果會顯示在 Snackbar
中。執行應用程式時,您可能會注意到 Flutter.dev 頁面看起來不太一樣。這是在啟用 JavaScript 的情況下執行的結果。
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
,以將訊息傳遞至已命名 JavascriptChannel
的 onMessageReceived
回呼處理常式。
如要使用上述新增的 JavaScript 頻道,請新增其他選單項目,以便在 JavaScript 環境中執行 XMLHttpRequest
,並使用 SnackBar
JavaScript 管道傳回結果。
現在 WebViewWidget
瞭解 JavaScript 管道了,
,接下來您要新增範例,進一步展開應用程式。為此,請將額外的 PopupMenuItem
新增至 Menu
類別,並新增額外的功能。
使用額外選單選項更新 _MenuOptions
,方法是新增 javascriptChannel
列舉值,然後將實作項目新增至 Menu
類別,如下所示:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel, // Add this option
}
class Menu extends StatefulWidget {
const Menu({required this.controller, super.key});
final WebViewController controller;
@override
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> {
@override
Widget build(BuildContext context) {
return PopupMenuButton<_MenuOptions>(
onSelected: (value) async {
switch (value) {
case _MenuOptions.navigationDelegate:
await widget.controller
.loadRequest(Uri.parse('https://youtube.com'));
case _MenuOptions.userAgent:
final userAgent = await widget.controller
.runJavaScriptReturningResult('navigator.userAgent');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('$userAgent'),
));
case _MenuOptions.javascriptChannel: // Add from here
await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
if (req.status == 200) {
let response = JSON.parse(req.responseText);
SnackBar.postMessage("IP Address: " + response.ip);
} else {
SnackBar.postMessage("Error: " + req.status);
}
}
req.send();'''); // To here.
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.userAgent,
child: Text('Show user-agent'),
),
const PopupMenuItem<_MenuOptions>( // Add from here
value: _MenuOptions.javascriptChannel,
child: Text('Lookup IP Address'),
), // To here.
],
);
}
}
當使用者選擇「JavaScript 頻道範例」選單選項時,系統就會執行這項 JavaScript。
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
if (req.status == 200) {
SnackBar.postMessage(req.responseText);
} else {
SnackBar.postMessage("Error: " + req.status);
}
}
req.send();
這個程式碼會將 GET
要求傳送至 Public IP Address API,傳回裝置的 IP 位址。在 SnackBar
JavascriptChannel
叫用 postMessage
,這個結果會顯示在 SnackBar
中。
11. 管理 Cookie
您的應用程式可以使用 CookieManager
類別管理 WebView
中的 Cookie。在這個步驟中,您將顯示 Cookie 清單、清除 Cookie 清單、刪除 Cookie,以及設定新 Cookie。針對每個 Cookie 用途,將項目新增至 _MenuOptions
,如下所示:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
// Add from here ...
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// ... to here.
}
此步驟的其餘變更著重在 Menu
類別,包括 Menu
類別的從無狀態轉換到有狀態。這項變更十分重要,因為 Menu
需要擁有 CookieManager
,而無狀態小工具中的可變動狀態是不佳的組合。
將 CookieManager 新增至產生的 State 類別,如下所示:
lib/src/menu.dart
class Menu extends StatefulWidget {
const Menu({required this.controller, super.key});
final WebViewController controller;
@override
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> {
final cookieManager = WebViewCookieManager(); // Add this line
@override
Widget build(BuildContext context) {
// ...
_MenuState
類別會包含先前新增至 Menu
類別的程式碼,以及新增的 CookieManager
。在後續章節中,您將為 _MenuState
新增輔助函式,而該函式又會叫用 尚未新增的選單項目。
取得完整 Cookie 清單
您將使用 JavaScript 取得所有 Cookie 的清單。為此,請在 _MenuState
類別的結尾新增名為 _onListCookies
的輔助方法。使用 runJavaScriptReturningResult
方法時,輔助方法會在 JavaScript 環境中執行 document.cookie
,傳回所有 Cookie 的清單。
將以下內容新增至 _MenuState
類別:
lib/src/menu.dart
Future<void> _onListCookies(WebViewController controller) async {
final String cookies = await controller
.runJavaScriptReturningResult('document.cookie') as String;
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(cookies.isNotEmpty ? cookies : 'There are no cookies.'),
),
);
}
清除所有 Cookie
如要清除 WebView 中的所有 Cookie,請使用 CookieManager
類別的 clearCookies
方法。如果 CookieManager
已清除 Cookie,此方法會傳回 Future<bool>
解析為 true
;如果沒有可清除的 Cookie,則傳回 false
。
將以下內容新增至 _MenuState
類別:
lib/src/menu.dart
Future<void> _onClearCookies() async {
final hadCookies = await cookieManager.clearCookies();
String message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There were no cookies to clear.';
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
新增 Cookie
呼叫 JavaScript 即可新增 Cookie。深入瞭解如何將 Cookie 加入 JavaScript 文件的 API。
將以下內容新增至 _MenuState
類別:
lib/src/menu.dart
Future<void> _onAddCookie(WebViewController controller) async {
await controller.runJavaScript('''var date = new Date();
date.setTime(date.getTime()+(30*24*60*60*1000));
document.cookie = "FirstName=John; expires=" + date.toGMTString();''');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Custom cookie added.'),
),
);
}
使用 CookieManager 設定 Cookie
您也可以使用 CookieManager 設定 Cookie,如下所示。
將以下內容新增至 _MenuState
類別:
lib/src/menu.dart
Future<void> _onSetCookie(WebViewController controller) async {
await cookieManager.setCookie(
const WebViewCookie(name: 'foo', value: 'bar', domain: 'flutter.dev'),
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Custom cookie is set.'),
),
);
}
移除 Cookie
移除 Cookie 時,您必須新增 Cookie,並將到期日設在過去。
將以下內容新增至 _MenuState
類別:
lib/src/menu.dart
Future<void> _onRemoveCookie(WebViewController controller) async {
await controller.runJavaScript(
'document.cookie="FirstName=John; expires=Thu, 01 Jan 1970 00:00:00 UTC" ');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Custom cookie removed.'),
),
);
}
新增 CookieManager 選單項目
接下來只要新增選單選項,再連接到剛才新增的輔助方法即可。請更新 _MenuState
類別,如下所示:
lib/src/menu.dart
class _MenuState extends State<Menu> {
final cookieManager = WebViewCookieManager();
@override
Widget build(BuildContext context) {
return PopupMenuButton<_MenuOptions>(
onSelected: (value) async {
switch (value) {
case _MenuOptions.navigationDelegate:
await widget.controller
.loadRequest(Uri.parse('https://youtube.com'));
case _MenuOptions.userAgent:
final userAgent = await widget.controller
.runJavaScriptReturningResult('navigator.userAgent');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('$userAgent'),
));
case _MenuOptions.javascriptChannel:
await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
if (req.status == 200) {
let response = JSON.parse(req.responseText);
SnackBar.postMessage("IP Address: " + response.ip);
} else {
SnackBar.postMessage("Error: " + req.status);
}
}
req.send();''');
case _MenuOptions.clearCookies: // Add from here
await _onClearCookies();
case _MenuOptions.listCookies:
await _onListCookies(widget.controller);
case _MenuOptions.addCookie:
await _onAddCookie(widget.controller);
case _MenuOptions.setCookie:
await _onSetCookie(widget.controller);
case _MenuOptions.removeCookie:
await _onRemoveCookie(widget.controller); // To here.
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.userAgent,
child: Text('Show user-agent'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.javascriptChannel,
child: Text('Lookup IP Address'),
),
const PopupMenuItem<_MenuOptions>( // Add from here
value: _MenuOptions.clearCookies,
child: Text('Clear cookies'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.listCookies,
child: Text('List cookies'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.addCookie,
child: Text('Add cookie'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.setCookie,
child: Text('Set cookie'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.removeCookie,
child: Text('Remove cookie'),
), // To here.
],
);
}
執行 CookieManager
如要使用您剛才新增的所有功能,請嘗試下列步驟:
- 選取「列出 Cookie」。其中應該會列出 flutter.dev 設定的 Google Analytics Cookie。
- 選取「清除 Cookie」。這樣應該就會回報 Cookie 確實已清除。
- 再次選取「清除 Cookie」。系統應會回報沒有可清除的 Cookie。
- 選取「列出 Cookie」。系統應該會回報沒有 Cookie。
- 選取「新增 Cookie」。將 Cookie 記錄為已加入。
- 選取「設定 Cookie」。將 Cookie 記錄為已設定。
- 選取「列出 Cookie」,最後再選取「移除 Cookie」。
12. 在 WebView 中載入 Flutter 資產、檔案和 HTML 字串
應用程式可以使用不同方法載入 HTML 檔案,並在 WebView 中顯示這些檔案。在這個步驟中,您將載入 pubspec.yaml
檔案中指定的 Flutter 素材資源,並載入位於指定路徑的檔案,並使用 HTML 字串載入網頁。
如要載入位於指定路徑的檔案,您必須將 path_provider
新增至 pubspec.yaml
。此為 Flutter 外掛程式,可在檔案系統中尋找常用位置。
在指令列中執行下列指令:
$ flutter pub add path_provider
如要載入素材資源,必須在 pubspec.yaml
中指定資產路徑。在 pubspec.yaml
中,新增以下幾行內容:
pubspec.yaml
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# Add from here
assets:
- assets/www/index.html
- assets/www/styles/style.css
# ... to here.
如要在專案中新增資產,請按照下列步驟操作:
- 在專案的根資料夾中新建名為
assets
的 Directory。 - 在
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
方法的方法,再次使用 WebViewController
,此方法會採用含有檔案路徑的 String
。
您必須先建立包含 HTML 程式碼的檔案。方法很簡單,只要在匯入作業下方的 menu.dart
檔案中,將 HTML 程式碼做為程式碼頂端的字串加到程式碼頂端即可。
import 'dart:io'; // Add this line,
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; // And this one.
import 'package:webview_flutter/webview_flutter.dart';
// Add from here ...
const String kExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Load file or HTML string example</title>
</head>
<body>
<h1>Local demo page</h1>
<p>
This is an example page used to demonstrate how to load a local file or HTML
string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
webview</a> plugin.
</p>
</body>
</html>
''';
// ... to here.
如要建立 File
並將 HTML 字串寫入檔案,您需要新增兩種方法。_onLoadLocalFileExample
會以字串形式提供路徑,以載入檔案,而 _prepareLocalFile()
方法會傳回該路徑。在程式碼中加入以下方法:
Future<void> _onLoadLocalFileExample(
WebViewController controller, BuildContext context) async {
final String pathToIndex = await _prepareLocalFile();
await controller.loadFile(pathToIndex);
}
static Future<String> _prepareLocalFile() async {
final String tmpDir = (await getTemporaryDirectory()).path;
final File indexFile = File('$tmpDir/www/index.html');
await Directory('$tmpDir/www').create(recursive: true);
await indexFile.writeAsString(kExamplePage);
return indexFile.path;
}
載入 HTML 字串
以提供 HTML 字串的方式顯示網頁是直接了當的。WebViewController
提供名為 loadHtmlString
的方法,可讓您提供 HTML 字串做為引數。WebView
會顯示提供的 HTML 網頁。在程式碼中加入以下方法:
Future<void> _onLoadFlutterAssetExample(
WebViewController controller, BuildContext context) async {
await controller.loadFlutterAsset('assets/www/index.html');
}
Future<void> _onLoadLocalFileExample(
WebViewController controller, BuildContext context) async {
final String pathToIndex = await _prepareLocalFile();
await controller.loadFile(pathToIndex);
}
static Future<String> _prepareLocalFile() async {
final String tmpDir = (await getTemporaryDirectory()).path;
final File indexFile = File('$tmpDir/www/index.html');
await Directory('$tmpDir/www').create(recursive: true);
await indexFile.writeAsString(kExamplePage);
return indexFile.path;
}
// Add here ...
Future<void> _onLoadHtmlStringExample(
WebViewController controller, BuildContext context) async {
await controller.loadHtmlString(kExamplePage);
}
// ... to here.
新增菜單品項
現在素材資源已設定完成且可供使用,且含有所有功能的方法後,就可以更新選單。將下列項目新增至 _MenuOptions
列舉:
lib/src/menu.dart
enum _MenuOptions {
navigationDelegate,
userAgent,
javascriptChannel,
listCookies,
clearCookies,
addCookie,
setCookie,
removeCookie,
// Add from here ...
loadFlutterAsset,
loadLocalFile,
loadHtmlString,
// ... to here.
}
列舉已更新後,您就可以新增選單選項,並將這些選項連接到剛剛新增的輔助方法。請更新 _MenuState
類別,如下所示:
lib/src/menu.dart
class _MenuState extends State<Menu> {
final cookieManager = WebViewCookieManager();
@override
Widget build(BuildContext context) {
return PopupMenuButton<_MenuOptions>(
onSelected: (value) async {
switch (value) {
case _MenuOptions.navigationDelegate:
await widget.controller
.loadRequest(Uri.parse('https://youtube.com'));
case _MenuOptions.userAgent:
final userAgent = await widget.controller
.runJavaScriptReturningResult('navigator.userAgent');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('$userAgent'),
));
case _MenuOptions.javascriptChannel:
await widget.controller.runJavaScript('''
var req = new XMLHttpRequest();
req.open('GET', "https://api.ipify.org/?format=json");
req.onload = function() {
if (req.status == 200) {
let response = JSON.parse(req.responseText);
SnackBar.postMessage("IP Address: " + response.ip);
} else {
SnackBar.postMessage("Error: " + req.status);
}
}
req.send();''');
case _MenuOptions.clearCookies:
await _onClearCookies();
case _MenuOptions.listCookies:
await _onListCookies(widget.controller);
case _MenuOptions.addCookie:
await _onAddCookie(widget.controller);
case _MenuOptions.setCookie:
await _onSetCookie(widget.controller);
case _MenuOptions.removeCookie:
await _onRemoveCookie(widget.controller);
case _MenuOptions.loadFlutterAsset: // Add from here
if (!mounted) return;
await _onLoadFlutterAssetExample(widget.controller, context);
case _MenuOptions.loadLocalFile:
if (!mounted) return;
await _onLoadLocalFileExample(widget.controller, context);
case _MenuOptions.loadHtmlString:
if (!mounted) return;
await _onLoadHtmlStringExample(widget.controller, context);
// To here.
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.userAgent,
child: Text('Show user-agent'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.javascriptChannel,
child: Text('Lookup IP Address'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.clearCookies,
child: Text('Clear cookies'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.listCookies,
child: Text('List cookies'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.addCookie,
child: Text('Add cookie'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.setCookie,
child: Text('Set cookie'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.removeCookie,
child: Text('Remove cookie'),
),
const PopupMenuItem<_MenuOptions>( // Add from here
value: _MenuOptions.loadFlutterAsset,
child: Text('Load Flutter Asset'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.loadHtmlString,
child: Text('Load HTML string'),
),
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.loadLocalFile,
child: Text('Load local file'),
), // To here.
],
);
}
測試素材資源、檔案和 HTML 字串
如要測試剛才實作的程式碼是否成功,您可以在自己的裝置上執行程式碼,然後點選其中一個新增的選單項目。請注意,_onLoadFlutterAssetExample
如何使用我們新增的 style.css
,將 HTML 檔案的標題變更為藍色。
13. 全部完成!
恭喜!您已完成程式碼研究室。您可以在程式碼研究室存放區中找到本程式碼研究室完成後的程式碼。
詳情請參閱其他 Flutter 程式碼研究室。