人气很高的flutter_webview_plugin,在打开多个WebView时会出错,而且缺少2个重要的功能:
- 不能在JS中调用Flutter方法
- 不能在H5进入某个URL之前拦截
虽然该插件不够完整,但是使用起来很方便,封装了很多功能,如果交互不多可以用该插件。
本文基于flutter官方webview_flutter实现JsBridge的简单封装,实现在H5页面点击按钮调起flutter弹窗,具体步骤如下:
环境搭建
1. Depend on it
Add this to your package’s pubspec.yaml file:
dependencies:
webview_flutter: ^0.3.22+1
2. Install it
You can install packages from the command line with Flutter:
$ flutter pub get
Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.
3. Import it
Now in your Dart code, you can use:
import 'package:webview_flutter/webview_flutter.dart';
4. For iOS,you need to add ios-Runner-Info.plist with
<key>io.flutter.embedded_views_preview</key>
<string>YES</string>
否则报错:
[VERBOSE-2:shell.cc(207)] Dart Error: Unhandled exception:
PlatformException(unregistered_view_type, trying to create a view with an unregistered type, unregistered view type: ‘plugins.flutter.io/webview’)
#0 StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:569:7)
#1 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:321:33)
#2 PlatformViewsService.initUiKitView (package:flutter/src/services/platform_views.dart:168:41)
#3 _UiKitViewState._createNewUiKitView (package:flutter/src/widgets/platform_view.dart:621:71)
#4 _UiKitViewState._initializeOnce (package:flutter/src/widgets/platform_view.dart:571:5)
#5 _UiKitViewState.didChangeDependencies (package:flutter/src/widgets/platform_view.dart:581:5)
#6 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4380:12)
#7 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4208:5)
#8 Element.inf<…>
代码实现
main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import './JsBridgeUtil.dart';
const String kNavigationExamplePage = '''
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Navigation Delegate Example</title>
<style type="text/css">
.click-button {
width: 300px;
height: 100px;
font-size: 40px;
}
</style>
</head>
<body>
<p style="color: #161823; font-size: 40px">
The test of method execution from H5 to Flutter
</p>
<Button class="click-button" onclick="executeMethod()">点我</Button>
</body>
<script defer type="text/javascript">
function executeMethod() {
let paramObject = {
method: 'showToast',
params: {
message: 'User Agent: ' + navigator.userAgent
}
}
ViaBridge.postMessage(JSON.stringify(paramObject));
}
</script>
</html>
''';
void main() => runApp(MaterialApp(home: WebViewExample()));
class WebViewExample extends StatefulWidget {
@override
_WebViewExampleState createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
// webview 加载进度设置
final Completer<WebViewController> _controller =
Completer<WebViewController>();
final String contentBase64 =
base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter WebView example'),
),
body: Builder(builder: (BuildContext context) {
return WebView(
// 要显示的url
initialUrl: 'data:text/html;base64,$contentBase64',
// JS执行模式 默认是disabled
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
// 在WebView创建完成后会产生一个 webViewController
_controller.complete(webViewController);
},
// 使用javascriptChannel JS可以调用Flutter
// javascriptChannels参数接受Set<JavascriptChannel>一个成员类型为JavascriptChannel的Set集合
javascriptChannels: <JavascriptChannel>[
_toasterJavascriptChannel(context),
].toSet(),
// 拦截请求
navigationDelegate: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
print('Page started loading: $url');
},
onPageFinished: (String url) {
print('Page finished loading: $url');
},
gestureNavigationEnabled: true,
);
}),
);
}
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'ViaBridge',
onMessageReceived: (JavascriptMessage message) async{
String jsonStr = message.message;
JsBridgeUtil.executeMethod(context, JsBridgeUtil.parseJson(jsonStr));
});
}
}
JsBridgeUtil.dart
import 'dart:convert';
import './JsBridge.dart';
import 'package:flutter/material.dart';
/*
* JsBridge执行类
**/
class JsBridgeUtil {
/// 将json字符串转化成对象
static JsBridge parseJson(String jsonStr) {
JsBridge jsBridgeModel = JsBridge.fromMap(jsonDecode(jsonStr));
return jsBridgeModel;
}
/// 向H5暴露接口调用
static executeMethod(BuildContext context, JsBridge jsBridge) async{
if (jsBridge.method == 'showToast') {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(jsBridge.params['message'])),
);
}
}
}
JsBridge.dart
/*
* 定义JsBridge的基本结构
**/
class JsBridge {
String method; // 方法名
Map params; // 传递数据
Function callback; // 执行回调
JsBridge(this.method, this.params, this.callback);
// jsonEncode方法中会调用实体类的这个方法。如果实体类中没有这个方法,会报错。
Map toJson() {
Map map = new Map();
map["method"] = this.method;
map["params"] = this.params;
map["callback"] = this.callback;
return map;
}
// jsonDecode(jsonStr)方法返回的是Map<String, dynamic>类型,需要这里将map转换成实体类
static JsBridge fromMap(Map<String, dynamic> map) {
JsBridge jsBridgeModel = new JsBridge(map['method'], map['params'], map['callback']);
return jsBridgeModel;
}
@override
String toString() {
return "JsBridge: {method: $method, params: $params, callback: $callback}";
}
}
在H5页面点击按钮“点我“就可以调起flutter弹窗,展示navigator.userAgent信息