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 开发环境
您需要使用两款软件才能完成此 Codelab:Flutter SDK 和一款编辑器。
您可以使用以下任一设备运行此 Codelab:
- 一台连接到计算机并设置为开发者模式的实体 Android 或 iOS 设备。
- iOS 模拟器(需要安装 Xcode 工具)。
- Android 模拟器(需要在 Android Studio 中设置)。
3. 开始使用
开始使用 Flutter
您可以通过多种方式创建新的 Flutter 项目,Android Studio 和 Visual 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 插件作为依赖项添加
使用 Pub 软件包可以轻松为 Flutter 应用添加额外的功能。在此 Codelab 中,您将向项目中添加 webview_flutter 插件。在终端中运行以下命令。
$ cd webview_in_flutter $ flutter pub add webview_flutter
如果您检查 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"
minSdkVersion 20 // MODIFY
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
4. 将 WebView 微件添加到 Flutter 应用
在此步骤中,您将向应用添加一个 WebView。WebView 是托管的原生视图,作为应用开发者,您可以选择如何在应用中托管这些原生视图。在 Android 上,您可以选择虚拟显示模式(目前为 Android 的默认设置)和混合集成模式。但 iOS 始终使用混合集成模式。
如需深入地了解虚拟显示模式与混合集成模式的差异,请参阅 Hosting native Android and iOS views in your Flutter app with Platform Views(使用平台视图在 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 应用以查看 WebView,其会显示 flutter.dev 网站。或者,在 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% 时,会有条件地使用 LinearProgressIndicator 覆盖 WebView。这涉及随时间变化的程序状态,您已将此状态存储在与 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 微件现在使用在周围 widget 中创建的控制器。这样,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();
},
),
],
);
}
}
此 widget 在构造时使用与其共享的 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。请注意,这种简单的实现还阻止了内嵌的 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'));
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.navigationDelegate,
child: Text('Navigate to YouTube'),
),
],
);
}
}
当用户选择 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(
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)菜单项。系统应会显示 SnackBar,通知您导航控制器已阻止导航到 YouTube。
|
|
9. JavaScript 求值
WebViewController 可以在当前页面的上下文中对 JavaScript 表达式求值。对 JavaScript 求值有两种不同方法:对于不返回值的 JavaScript 代码,使用 runJavaScript;对于返回值的 JavaScript 代码,使用 runJavaScriptReturningResult。
如需启用 JavaScript,您需要配置 WebViewController,并将 javaScriptMode 属性设置为 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 代码。在此步骤中,您将注册一个使用 XMLHttpRequest 的结果调用的 SnackBar 渠道。
将 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 渠道,接下来添加一个示例以进一步扩展该应用。为此,请向 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 渠道示例)菜单选项时,系统会执行此 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();
此代码会向公共 IP 地址 API 发送 GET 请求,并返回设备的 IP 地址。对 SnackBar JavascriptChannel 调用 postMessage,系统会在 SnackBar 中显示结果。
11. 管理 Cookie
您的应用可以使用 CookieManager 类管理 WebView 中的 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 的列表。为此,请在名为 _onListCookies 的 _MenuState 类的末尾添加一个辅助方法。使用 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,此方法会返回一个解析为 true 的 Future<bool>;如果没有要清除的 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。用于向 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.'),
),
);
}
使用 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'));
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
如需使用您刚刚添加到应用中的所有功能,请尝试执行以下步骤:
- 选择 List cookies(列出 Cookie)。系统应会列出 flutter.dev 设置的 Google Analytics(分析)Cookie。
- 选择 Clear cookies(清除 Cookie)。系统应会报告相应 Cookie 确实已被清除。
- 再次选择 Clear cookies(清除 Cookie)。系统应会报告没有可清除的任何 Cookie。
- 选择 List cookies(列出 Cookie)。系统应会报告没有 Cookie。
- 选择 Add cookie(添加 Cookie)。系统应会报告 Cookie 已添加。
- 选择 Set cookie(设置 Cookie)。系统应会报告 Cookie 已设置。
- 选择 List cookies(列出 Cookie),最后,选择 Remove 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.
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的新 Directory。 - 在
www文件夹中创建一个名为styles的新 Directory。 - 在
www文件夹中创建一个名为index.html的新 File。 - 在
styles文件夹中创建一个名为style.css的新 File。
复制以下代码并将其粘贴到 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'));
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。















