欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、webview_flutter 库简介
webview_flutter 是 Flutter 官方维护的 WebView 插件,允许在 Flutter 应用中嵌入原生 WebView 组件。无论是展示网页内容、集成第三方服务、还是实现混合开发,webview_flutter 都是最核心的解决方案。
📋 webview_flutter 核心特点
| 特点 | 说明 |
|---|---|
| 网页加载 | 支持加载 URL、HTML 字符串、本地文件 |
| JavaScript 交互 | 支持执行 JavaScript 和 Flutter-JS 双向通信 |
| 导航控制 | 支持前进、后退、刷新、拦截导航请求 |
| Cookie 管理 | 支持获取和设置 WebView Cookie |
| 权限管理 | 支持处理网页权限请求(摄像头、麦克风等) |
| 进度监听 | 支持监听页面加载进度 |
| 跨平台兼容 | 支持 Android、iOS、Web、OpenHarmony |
平台功能支持对比
| 功能 | Android | iOS | Web | OpenHarmony |
|---|---|---|---|---|
| 加载 URL | ✔️ | ✔️ | ✔️ | ✔️ |
| 加载 HTML | ✔️ | ✔️ | ✔️ | ✔️ |
| JavaScript 执行 | ✔️ | ✔️ | ✔️ | ✔️ |
| JS 通道通信 | ✔️ | ✔️ | ✔️ | ✔️ |
| 导航拦截 | ✔️ | ✔️ | ✔️ | ✔️ |
| Cookie 管理 | ✔️ | ✔️ | ✔️ | ✔️ |
| 进度监听 | ✔️ | ✔️ | ✔️ | ✔️ |
| 权限请求 | ✔️ | ✔️ | ❌ | ✔️ |
使用场景
- 嵌入第三方网页内容
- 集成在线文档查看器
- 混合开发(Flutter + H5)
- 内嵌视频播放(B站、虎牙等)
- 在线支付页面
- 用户协议/隐私政策展示
二、OpenHarmony 适配版本
2.1 环境说明
| 组件 | 版本 |
|---|---|
| Flutter | 3.27.5 |
| HarmonyOS | 6.0 |
| webview_flutter | 4.13.0 (OpenHarmony 适配版本) |
2.2 引入方式
在 pubspec.yaml 文件中添加以下依赖配置:
yaml
dependencies:
flutter:
sdk: flutter
# webview_flutter OpenHarmony 适配版本
webview_flutter:
git:
url: https://atomgit.com/openharmony-tpc/flutter_packages
path: packages/webview_flutter/webview_flutter
ref: br_webview_flutter-v4.13.0_ohos
2.3 获取依赖
配置完成后,在项目根目录执行:
bash
flutter pub get
2.4 权限配置
如果 WebView 需要访问网络,需要配置网络权限:
打开 ohos/entry/src/main/module.json5,在 requestPermissions 中添加:
json
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:network_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
在 ohos/entry/src/main/resources/base/element/string.json 中添加:
json
{
"name": "network_reason",
"value": "使用网络加载网页内容"
}
三、核心 API 讲解
3.1 WebViewController 类
WebViewController 是 WebView 的核心控制器,负责管理网页加载、导航、JavaScript 执行等操作。
构造方法
dart
WebViewController({
void Function(WebViewPermissionRequest request)? onPermissionRequest,
})
主要方法详解
3.1.1 loadRequest - 加载 URL
dart
Future<void> loadRequest(
Uri uri, {
LoadRequestMethod method = LoadRequestMethod.get,
Map<String, String> headers = const <String, String>{},
Uint8List? body,
})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| uri | Uri | 是 | - | 要加载的网页 URL |
| method | LoadRequestMethod | 否 | LoadRequestMethod.get | HTTP 请求方法(GET/POST) |
| headers | Map<String, String> | 否 | {} | 自定义请求头 |
| body | Uint8List? | 否 | null | POST 请求体 |
使用示例:
dart
final controller = WebViewController();
await controller.loadRequest(Uri.parse('https://www.bilibili.com'));
3.1.2 loadHtmlString - 加载 HTML 字符串
dart
Future<void> loadHtmlString(String html, {String? baseUrl})
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| html | String | 是 | - | HTML 内容字符串 |
| baseUrl | String? | 否 | null | 基础 URL,用于解析相对路径 |
3.1.3 loadFlutterAsset - 加载本地资源
dart
Future<void> loadFlutterAsset(String key)
参数说明:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| key | String | 是 | - | pubspec.yaml 中声明的资源路径 |
3.1.4 导航控制方法
dart
// 检查是否可以后退
Future<bool> canGoBack()
// 检查是否可以前进
Future<bool> canGoForward()
// 后退
Future<void> goBack()
// 前进
Future<void> goForward()
// 刷新
Future<void> reload()
// 获取当前 URL
Future<String?> currentUrl()
3.1.5 JavaScript 相关方法
dart
// 设置 JavaScript 模式
Future<void> setJavaScriptMode(JavaScriptMode javaScriptMode)
// 执行 JavaScript(无返回值)
Future<void> runJavaScript(String javaScript)
// 执行 JavaScript 并获取返回值
Future<Object> runJavaScriptReturningResult(String javaScript)
// 添加 JavaScript 通道
Future<void> addJavaScriptChannel(
String name, {
required void Function(JavaScriptMessage) onMessageReceived,
})
// 移除 JavaScript 通道
Future<void> removeJavaScriptChannel(String javaScriptChannelName)
JavaScriptMode 枚举:
dart
enum JavaScriptMode {
disabled, // 禁用 JavaScript
unrestricted, // 启用 JavaScript
}
3.1.6 缩放控制
dart
// 启用/禁用缩放
Future<void> enableZoom(bool enabled)
3.1.7 缓存管理
dart
// 清除所有缓存
Future<void> clearCache()
// 清除本地存储
Future<void> clearLocalStorage()
3.1.8 其他方法
dart
// 获取页面标题
Future<String?> getTitle()
// 设置背景颜色
Future<void> setBackgroundColor(Color color)
// 设置 User-Agent
Future<void> setUserAgent(String? userAgent)
// 获取 User-Agent
Future<String?> getUserAgent()
// 滚动到指定位置
Future<void> scrollTo(int x, int y)
// 滚动指定距离
Future<void> scrollBy(int x, int y)
// 获取当前滚动位置
Future<Offset> getScrollPosition()
3.2 NavigationDelegate 类
NavigationDelegate 用于监听和控制 WebView 的导航行为。
构造方法
dart
NavigationDelegate({
FutureOr<NavigationDecision> Function(NavigationRequest request)? onNavigationRequest,
void Function(String url)? onPageStarted,
void Function(String? url)? onPageFinished,
void Function(int progress)? onProgress,
void Function(WebResourceError error)? onWebResourceError,
})
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
| onNavigationRequest | 回调函数 | 导航请求拦截,返回 NavigationDecision.navigate 或 NavigationDecision.prevent |
| onPageStarted | 回调函数 | 页面开始加载时触发 |
| onPageFinished | 回调函数 | 页面加载完成时触发 |
| onProgress | 回调函数 | 页面加载进度(0-100) |
| onWebResourceError | 回调函数 | 资源加载错误时触发 |
NavigationDecision 枚举:
dart
enum NavigationDecision {
navigate, // 允许导航
prevent, // 阻止导航
}
3.3 WebViewWidget 类
WebViewWidget 是用于在 Flutter UI 中显示 WebView 的组件。
dart
WebViewWidget({
Key? key,
required WebViewController controller,
TextDirection layoutDirection = TextDirection.ltr,
Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers = const {},
})
3.4 WebViewCookieManager 类
WebViewCookieManager 用于管理 WebView 的 Cookie。
dart
final cookieManager = WebViewCookieManager();
// 设置 Cookie
await cookieManager.setCookie(WebViewCookie(
name: 'session_id',
value: 'abc123',
domain: '.example.com',
));
// 获取 Cookie
final cookies = await cookieManager.getCookies();
// 清除 Cookie
await cookieManager.clearCookies();
3.5 JavaScriptConsoleMessage 类
用于接收 WebView 的 JavaScript 控制台日志。
dart
controller.setOnConsoleMessage((JavaScriptConsoleMessage message) {
print('JS Console: ${message.level} - ${message.message}');
});
3.6 JavaScript 对话框处理
dart
// 处理 alert 对话框
controller.setOnJavaScriptAlertDialog((JavaScriptAlertDialogRequest request) async {
// 显示自定义对话框
return;
});
// 处理 confirm 对话框
controller.setOnJavaScriptConfirmDialog((JavaScriptConfirmDialogRequest request) async {
// 返回 true 或 false
return true;
});
// 处理 text input 对话框
controller.setOnJavaScriptTextInputDialog((JavaScriptTextInputDialogRequest request) async {
// 返回用户输入的文本
return '用户输入';
});
四、完整使用示例
以下是一个应用级别的网页浏览器应用,整合了网页浏览、导航控制、进度显示、多网站切换等功能:

dart
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
runApp(const WebViewBrowserApp());
}
class WebViewBrowserApp extends StatelessWidget {
const WebViewBrowserApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView 浏览器',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00B4FF)),
useMaterial3: true,
),
home: const WebViewBrowserHomePage(),
);
}
}
class WebViewBrowserHomePage extends StatefulWidget {
const WebViewBrowserHomePage({super.key});
@override
State<WebViewBrowserHomePage> createState() => _WebViewBrowserHomePageState();
}
class _WebViewBrowserHomePageState extends State<WebViewBrowserHomePage> {
late final WebViewController _controller;
int _progress = 0;
String _currentUrl = '';
String _pageTitle = '';
bool _isLoading = true;
final TextEditingController _urlController = TextEditingController();
// 预设网站列表
final List<Map<String, dynamic>> _presetWebsites = [
{
'name': '哔哩哔哩',
'url': 'https://m.bilibili.com',
'icon': Icons.play_circle_outline,
'color': const Color(0xFFFB7299),
},
{
'name': 'CSDN',
'url': 'https://m.csdn.net',
'icon': Icons.code,
'color': const Color(0xFFFF6633),
},
{
'name': '虎牙直播',
'url': 'https://m.huya.com',
'icon': Icons.live_tv,
'color': const Color(0xFFFF4500),
},
{
'name': '百度',
'url': 'https://m.baidu.com',
'icon': Icons.search,
'color': const Color(0xFF306CFF),
},
];
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
setState(() {
_progress = progress;
_isLoading = progress < 100;
});
},
onPageStarted: (String url) {
setState(() {
_isLoading = true;
_currentUrl = url;
_urlController.text = url;
});
},
onPageFinished: (String url) {
setState(() {
_isLoading = false;
_currentUrl = url;
});
_updatePageTitle();
},
onWebResourceError: (WebResourceError error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('加载错误: ${error.description}'),
backgroundColor: Colors.red,
),
);
}
},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
)
..setOnConsoleMessage((JavaScriptConsoleMessage message) {
debugPrint('WebView Console: ${message.message}');
});
// 默认加载 B站
_controller.loadRequest(Uri.parse(_presetWebsites[0]['url']));
_urlController.text = _presetWebsites[0]['url'];
}
Future<void> _updatePageTitle() async {
final title = await _controller.getTitle();
if (mounted) {
setState(() {
_pageTitle = title ?? '';
});
}
}
Future<void> _loadUrl(String url) async {
String finalUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
finalUrl = 'https://$url';
}
await _controller.loadRequest(Uri.parse(finalUrl));
}
Future<void> _goBack() async {
if (await _controller.canGoBack()) {
await _controller.goBack();
}
}
Future<void> _goForward() async {
if (await _controller.canGoForward()) {
await _controller.goForward();
}
}
Future<void> _reload() async {
await _controller.reload();
}
void _showWebsitePicker() {
showModalBottomSheet(
context: context,
builder: (context) => Container(
padding: const EdgeInsets.all(16),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.5,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'选择网站',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Flexible(
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 0.8,
),
itemCount: _presetWebsites.length,
itemBuilder: (context, index) {
final website = _presetWebsites[index];
return GestureDetector(
onTap: () {
Navigator.pop(context);
_loadUrl(website['url']);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 24,
backgroundColor: website['color'],
child: Icon(
website['icon'],
color: Colors.white,
size: 24,
),
),
const SizedBox(height: 6),
Text(
website['name'],
style: const TextStyle(fontSize: 11),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
const SizedBox(height: 16),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_pageTitle.isNotEmpty ? _pageTitle : 'WebView 浏览器',
style: const TextStyle(fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (_currentUrl.isNotEmpty)
Text(
_currentUrl,
style: const TextStyle(fontSize: 10, color: Colors.white70),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
backgroundColor: const Color(0xFF00B4FF),
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.grid_view),
onPressed: _showWebsitePicker,
tooltip: '网站列表',
),
],
),
body: Column(
children: [
// 进度条
if (_isLoading)
LinearProgressIndicator(
value: _progress / 100,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B4FF)),
),
// URL 输入栏
_buildUrlBar(),
// WebView
Expanded(
child: WebViewWidget(controller: _controller),
),
// 底部导航栏
_buildBottomNavigationBar(),
],
),
);
}
Widget _buildUrlBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: Colors.white,
child: Row(
children: [
Expanded(
child: TextField(
controller: _urlController,
decoration: InputDecoration(
hintText: '输入网址',
prefixIcon: const Icon(Icons.language, size: 20),
suffixIcon: IconButton(
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
_urlController.clear();
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(vertical: 0),
isDense: true,
),
onSubmitted: (value) {
if (value.isNotEmpty) {
_loadUrl(value);
}
},
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
if (_urlController.text.isNotEmpty) {
_loadUrl(_urlController.text);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00B4FF),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: const Text('前往'),
),
],
),
);
}
Widget _buildBottomNavigationBar() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavButton(
icon: Icons.arrow_back,
label: '后退',
onTap: _goBack,
),
_buildNavButton(
icon: Icons.arrow_forward,
label: '前进',
onTap: _goForward,
),
_buildNavButton(
icon: Icons.refresh,
label: '刷新',
onTap: _reload,
),
_buildNavButton(
icon: Icons.home,
label: '首页',
onTap: () => _loadUrl(_presetWebsites[0]['url']),
),
_buildNavButton(
icon: Icons.share,
label: '分享',
onTap: _shareUrl,
),
],
),
);
}
Widget _buildNavButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24, color: const Color(0xFF00B4FF)),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(fontSize: 10, color: Colors.grey),
),
],
),
),
);
}
void _shareUrl() {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('当前 URL: $_currentUrl')),
);
}
@override
void dispose() {
_urlController.dispose();
super.dispose();
}
}
五、适配要点
5.1 OpenHarmony 平台特性
-
权限处理
- WebView 需要网络权限才能加载网页
- 如果网页请求摄像头/麦克风权限,需要在
module.json5中声明相应权限 - 权限请求会通过
onPermissionRequest回调通知应用
-
JavaScript 交互
- OpenHarmony 平台完全支持 JavaScript 执行
- JavaScript 通道(JavaScriptChannel)可用于 Flutter 与网页的双向通信
- 建议设置
JavaScriptMode.unrestricted以启用完整功能
-
页面加载
- 使用
onProgress回调可以实时获取加载进度 onPageFinished在页面完全加载后触发onWebResourceError可以捕获加载错误
- 使用
-
导航控制
onNavigationRequest可以拦截 URL 跳转- 返回
NavigationDecision.prevent可以阻止跳转 - 适用于处理自定义 URL Scheme
5.2 与 Android/iOS 的差异
| 差异点 | Android/iOS | OpenHarmony |
|---|---|---|
| JavaScript 对话框 | 原生对话框 | 需要自定义处理 |
| Cookie 管理 | 完整支持 | 支持 |
| 文件上传 | 支持 | 支持 |
| 地理位置 | 支持 | 支持 |
| 视频播放 | 支持 | 支持,但部分网站可能需要特殊处理 |
| User-Agent | 可自定义 | 可自定义 |
5.3 注意事项
-
HTTPS 混合内容
- 部分网站可能混合加载 HTTP 和 HTTPS 内容
- OpenHarmony 默认可能阻止混合内容,需要特别配置
-
视频播放
- B站、虎牙等视频网站可能需要启用硬件加速
- 部分视频格式可能需要额外的解码器支持
-
内存管理
- WebView 占用内存较大,不使用时应及时释放
- 避免同时创建多个 WebView 实例
-
安全考虑
- 不要加载不可信的网页内容
- 使用
onNavigationRequest拦截可疑跳转 - 敏感操作建议在前端验证
六、常见问题
Q1: WebView 加载空白页面?
原因: 网络权限未配置或 URL 不正确。
解决方案:
- 检查
module.json5中是否添加了ohos.permission.INTERNET权限 - 确认 URL 格式正确(包含 http:// 或 https://)
- 检查网络连接是否正常
Q2: JavaScript 不生效?
原因: JavaScript 模式未启用。
解决方案:
dart
final controller = WebViewController();
controller.setJavaScriptMode(JavaScriptMode.unrestricted);
Q3: 如何实现 Flutter 与网页的双向通信?
解决方案:
dart
// Flutter 端添加 JavaScript 通道
controller.addJavaScriptChannel(
'FlutterChannel',
onMessageReceived: (JavaScriptMessage message) {
print('收到网页消息: ${message.message}');
// 处理消息
},
);
// 网页端调用
// FlutterChannel.postMessage('Hello from Web');
// Flutter 调用网页 JavaScript
controller.runJavaScript('window.receiveMessage("Hello from Flutter")');
Q4: 如何拦截特定 URL 的跳转?
解决方案:
dart
controller.setNavigationDelegate(
NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
// 拦截特定域名
if (request.url.contains('blocked-domain.com')) {
return NavigationDecision.prevent;
}
// 拦截特定协议
if (request.url.startsWith('tel:')) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
);
Q5: 视频无法播放?
原因: 可能需要启用硬件加速或网站不支持。
解决方案:
- 确保使用 HTTPS 协议的视频网站
- 尝试使用移动版网站(如 m.bilibili.com)
- 检查网站是否需要登录才能播放
Q6: 如何获取当前页面的标题和 URL?
解决方案:
dart
// 获取标题
final title = await controller.getTitle();
// 获取当前 URL
final url = await controller.currentUrl();
Q7: 如何处理网页的 alert/confirm 对话框?
解决方案:
dart
controller.setOnJavaScriptAlertDialog((JavaScriptAlertDialogRequest request) async {
// 显示自定义对话框
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: Text(request.message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
});
controller.setOnJavaScriptConfirmDialog((JavaScriptConfirmDialogRequest request) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认'),
content: Text(request.message),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确定'),
),
],
),
);
return result ?? false;
});
七、总结
webview_flutter 是 Flutter 生态中最常用的 WebView 插件,在 OpenHarmony 平台的适配已经非常成熟。本文总结了:
- webview_flutter 的核心 API 和使用方法
- WebViewController 的网页加载、导航控制、JavaScript 交互等功能
- NavigationDelegate 的导航拦截和事件监听
- 完整的应用级别浏览器实现
- OpenHarmony 平台的权限配置和适配要点
- 常见问题和解决方案
在实际开发中,建议根据具体需求合理配置 JavaScript 模式和导航拦截,注意内存管理和安全性。对于视频播放等复杂场景,建议使用移动版网站以获得更好的兼容性。
💡 提示: 更多 OpenHarmony 适配的 Flutter 三方库信息,请访问 开源鸿蒙跨平台开发者社区 获取最新资源和技术支持。