Flutter 使用Webview进行混合开发前言 开发过程中我们会遇到App嵌套网页的使用场景,我们在App中跳转到H
前言
开发过程中我们会遇到App嵌套网页的使用场景,我们在App中跳转到H5网页有时候网页中会有跳转到App的需求,也就是App与网页的相互通信。下面我们来慢慢的实现这个功能。
准备工作
安装Flutter插件webview_flutter
pubspec.yaml添加依赖
开发使用的Flutter版本是2.2.3,dart版本是2.13.x,安装
webview_flutter: ^2.0.10
最低dart版本>=2.12.x
,建议使用新的版本,之前有遇到安卓手机不能弹起输入键盘、内存泄漏、文本不能复制等一系列问题,2.0.10
这个版本没有遇见类似问题。
dependencies:
...
webview_flutter: ^2.0.10
..
使用webview_flutter
插件
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_nxj_c/helpers/colors.dart';
import 'package:flutter_nxj_c/helpers/config.dart';
import 'package:flutter_nxj_c/stores/hybrid_h5.dart';
import 'package:webview_flutter/webview_flutter.dart';
class HybridH5 extends StatefulWidget {
static const String routeName = '/hybrid_h5';
const HybridH5();
@override
_HybridH5State createState() => _HybridH5State();
}
class _HybridH5State extends State<HybridH5> {
final Completer<WebViewController> _controller = Completer<WebViewController>();
final _hybridH5Store = HybridH5Store();
late WebViewController _webViewController;
String title = '加载中...';
@override
void initState() {
super.initState();
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
Future(() async {
// 请求参数参数
var params = ModalRoute.of(context)!.settings.arguments;
debugPrint('WebView参数-----$params');
// Modal.loading(duration: Duration.zero);
await _initController();
});
}
Future<void> _initController() async {
await _controller.future.then((controller) {
_webViewController = controller;
_webViewController.loadUrl('https://juejin.cn/user/1046390798028072');
});
}
@override
Widget build(BuildContext context) => WillPopScope(
onWillPop: () async {
var readyController = await _controller.future;
if (await readyController.canGoBack()) {
await readyController.goBack();
return Future.value(false);
}
return Future.value(true);
},
child: CupertinoPageScaffold(
backgroundColor: ColorTheme.of(context).colorF3F3F6,
navigationBar: CupertinoNavigationBar(
backgroundColor: ColorTheme.of(context).colorFFFFFF,
padding: EdgeInsetsDirectional.zero,
border: Border.all(color: ColorTheme.of(context).borderColor),
transitionBetweenRoutes: Platform.isIOS,
middle: Text('$title'),
leading: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
var readyController = await _controller.future;
if (await readyController.canGoBack()) {
await readyController.goBack();
return;
}
Navigator.pop(context, '数据传参');
},
child: Container(
width: 42.0,
padding: const EdgeInsets.only(left: 10.0, right: 20.0),
child: Image.asset(
'assets/icons/ic_arrow_left_gray.png',
color: ColorTheme.of(context).color202326,
),
),
),
),
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (webViewController) {
_controller.complete(webViewController);
},
onProgress: (progress) {
print('WebView is loading (progress : $progress%)');
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
onPageStarted: (url) {
print('Page started loading: $url');
},
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('标题--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
},
gestureNavigationEnabled: true,
),
),
);
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (message) {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
}
初始化加载地址,设置headers
使用_webViewController.loadUrl
方法加载网址,这个地方我们可以headers
,有token需求的同学们就可以直接将App内的token放到浏览器中,我们也可以访问cookie
Future<void> _initController() async {
await _controller.future.then((controller) {
_webViewController = controller;
_webViewController.loadUrl('https://juejin.cn/user/1046390798028072',headers:{});
});
}
Webview
组件构建
当webview
生成成功以后调用_controller.complete(webViewController)
将我们需要访问的网址放入,浏览器就会访问指定的网页
WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (webViewController) {
_controller.complete(webViewController);
},
onProgress: (progress) {
print('WebView is loading (progress : $progress%)');
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (request) => _hybridH5Store.listenNavigationDelegate(request),
onPageStarted: (url) {
print('Page started loading: $url');
},
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('标题--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
},
gestureNavigationEnabled: true,
)
获取浏览器访问的页面title
在浏览器访问网页的过程中我们会修改标题,达到不同页面显示不同标题的功能。
调用_webViewController.evaluateJavascript('document.title')
方法获取网页标题。
注意:这是一个异步方法。
onPageFinished: (url) async {
debugPrint('Page finished loading: $url');
await _webViewController.evaluateJavascript('document.title').then((result) {
debugPrint('标题--: $result');
if (result.replaceAll('"', '').isNotEmpty) {
setState(() => title = result.replaceAll('"', ''));
}
});
}
浏览器网页与App的相互通信
App如何接收到网页的方法
WebView
增加javascriptChannels
,javascriptChannels
是javaScript
的管道可以包含很多个自己定义的方法。
javascriptChannels: <JavascriptChannel>{
// 弹出App内的提示框
_toasterJavascriptChannel(context),
// 保存图片到相册
_fileDownLoaderChannel(),
// 添加自定义的方法处理网页
...
},
_toasterJavascriptChannel
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (message) {
// ignore: deprecated_member_use
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
_fileDownLoaderChannel
JavascriptChannel _fileDownLoaderChannel() {
return JavascriptChannel(
name: 'fileDownLoader',
onMessageReceived: (message) async {
// 跳转到指定页面
try {
final data = json.decode(message.message) as Map<String, dynamic>;
var response = await api.Interceptor.dio
.get<Options>("${data['url']}", options: Options(responseType: ResponseType.bytes));
await ImageGallerySaver.saveImage(Uint8List.fromList(response.data as List<int>));
Toast.info(msg: '保存图片成功', showTime: 5000);
} catch (err) {
Toast.info(msg: '保存图片失败', showTime: 5000);
}
});
}
网页通知App弹出提示框、保存图片到相册
// 保存图片到App
window.fileDownLoader.postMessage(JSON.stringify({ url: detailData.codeUrl }));
// 使用App的提示
window.Toaster.postMessage(JSON.stringify({ message: '提示信息' }));
JavascriptChannel
的name
就是window
方法需要通信的方法名,onMessageReceived
方法的参数就是postMessage
发送的参数。
网页不能打开三方应用问题
网页中会有打开第三方应用、拨打电话的功能,在我们不对WebView
增加方法的时候点击就会无效。话不多说现在开始解决这个问题。
WebView
中增加navigationDelegate
这个方法可以监听到导航地址的变化。
navigationDelegate: (NavigationRequest request) {
debugPrint('request.url ${request.url}');
// 检查支付宝
if (request.url.contains('alipays://')) {
_openUrl(request.url);
return NavigationDecision.prevent;
}
// 路由拦截-单页应用路由检测有问题
if (request.url.contains('https://3gimg.qq.com/') ||
request.url.contains('https://apis.map.qq.com')) {
debugPrint('跳转地图-路由被拦截');
return NavigationDecision.prevent;
}
if (request.url.contains('tel:')) {
// 拨打电话
_openUrl(request.url);
return NavigationDecision.prevent;
}
debugPrint('allowing navigation to ${request.url}');
return NavigationDecision.navigate;
}
_openUrl方法
使用url_launcher
插件的launch
方法可以打开App或者浏览器,打开App使用的是schema
。
Future<void> _openUrl(String linkUrl) async {
await launch(linkUrl);
}
结语
目前就是我使用Webview
遇到的问题,大家遇到有其它问题欢迎留言讨论。
转载自:https://juejin.cn/post/6994088899349856263