跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
前言
跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget
Flutter作为基础的应用,如果要在flutter 中嵌入webview 去做Hybrid混合开发,咱们就必须要封装一套易用的webview,但网上关于flutter webview的文章极其的少。但的确也有做封装的文章,但是封装手法不够优雅,封装效果不够扩展。于是我打算把我的封装与大家分享,看我如何做到高扩展,高易用性。
目标:
Flutter 中嵌入 webview ,能够与 flutter 双向通信,并且易用。
搭建前夕准备
三方库:
webview_flutter flutter网页widget
开始搭建
一、基本回调
1.1 webview外部基本管理器
typedef void InnerWebPageCreatedCallback(InnerWebPageController controller);
涉及到的管理器源码之后介绍
1.2 显示处理回调
typedef WebPageCallBack = Function(String name,dynamic value);
由url拦截器,或者js返回数据后调用flutter页面代码,可以更新页面,或者状态变更。
使用如下:
webPageCallBack = (String name,dynamic value){
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}
};
1.3 url拦截器处理
typedef WebPageUrlIntercept = bool Function(String url,InnerWebPageController? _controller);
一般用于处理 请求的url中的特殊文字处理
1.4 网页title 回调
typedef TitleCallBack = Function(String title);
网页加载完成后调用该回调展示当前html 的 title标签
二、构建web widget 控制管理器
class InnerWebPageController {
//webview原有管理器
WebViewController _controller;
InnerWebPageController(this._controller);
//执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascript(String funname,List? param,bool brackets) async{
String javaScriptString = getJavaScriptString(funname,param,brackets);
await _controller.runJavascript(javaScriptString);
}
//带返回值执行网页js,在原有基础上封装,只需要发送jsname与参数
Future runJavascriptReturningResult(String funname,List? param,bool brackets) async {
String javaScriptString = getJavaScriptString(funname,param,brackets);
_controller.runJavascript(javaScriptString);
return await _controller.runJavascriptReturningResult(javaScriptString);
}
//是否可以返回
Future canGoBack() {
return _controller.canGoBack();
}
//返回网页历史
Future goBack() {
return _controller.goBack();
}
//重新加载
Future reload() {
LibLoading.show();
return _controller.reload();
}
//获取js请求(工具)
String getJavaScriptString(String funname, List? param,bool brackets) {
var strb = StringBuffer(funname);
if(brackets){
strb.write("(");
}
if(param!=null&¶m.length>0){
for(int i=0;i ${strb.toString()}");
return strb.toString();
}
}
;i++){>
三、构建JavascriptChannels js注册抽象基础类
abstract class JavascriptChannels{
WebPageCallBack? webPageCallBack;
InnerWebPageController? controller;
JavascriptChannels();
//log日志
void logFunctionName(String functionName, String data) {
ULog.d("JS functionName -> $functionName JS params -> $data");
}
Set? baseJavascriptChannels(BuildContext context){
var javascriptChannels = {
_alertJavascriptChannel(context),
};
var other = otherJavascriptChannels(context);
if(other!=null){
javascriptChannels.addAll(other);
}
return javascriptChannels;
}
//lib库基本方法
JavascriptChannel _alertJavascriptChannel(BuildContext context) {
var jname = 'Toast';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
TipToast.instance.tip(message.message);
});
}
//实现类实现方法
Set? otherJavascriptChannels(BuildContext context);
}
三、构建UrlIntercept url拦截抽象基础类
abstract class UrlIntercept{
WebPageCallBack? webPageCallBack;
WebPageUrlIntercept _webPageUrlIntercept;
InnerWebPageController? controller;
UrlIntercept(this._webPageUrlIntercept);
//基本拦截
bool baseUrlIntercept(String url){
ULog.d('intercept: ${url}');
return _libUrlIntercept( url)||otherUrlIntercept( url);
}
//其他拦截
bool otherUrlIntercept(String url) {
return _webPageUrlIntercept.call(url,controller);
}
//lib 库默认拦截
bool _libUrlIntercept(String url) {
return _openPay(url);
}
// 跳转外部支付
bool _openPay(String url) {
if (url.startsWith('alipays:') || url.startsWith('weixin:')) {
canLaunch(url).then((value) => {
if(value){
launch(url)
}else{
TipToast.instance.tip('未安装支付软件')
}
});
return true;
}
return false;
}
}
四、webview widget实现
class InnerWebPage extends StatefulWidget{
String _url;
TitleCallBack? _titleCallBack;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
InnerWebPageCreatedCallback? _onInnerWebPageCreated;
WebResourceErrorCallback? _onWebResourceError;
InnerWebPage(String url,{TitleCallBack? titleCallBack,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,InnerWebPageCreatedCallback? onInnerWebPageCreated,WebResourceErrorCallback? onWebResourceError}):_url = url,_titleCallBack = titleCallBack,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,_onInnerWebPageCreated = onInnerWebPageCreated,_onWebResourceError = onWebResourceError;
@override
State createState() => _InnerWebPageState();
}
class _InnerWebPageState extends State {
late WebViewController _controller;
InnerWebPageController? _innercontroller;
@override
void initState() {
super.initState();
// Android端复制粘贴问题
if (Platform.isAndroid) {
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
}
@override
Widget build(BuildContext context) {
return WebView(
onWebViewCreated: (controller){
ULog.i("WebView is create");
LibLoading.show();
_controller = controller;
_innercontroller = InnerWebPageController(_controller);
widget._onInnerWebPageCreated?.call(_innercontroller!);
widget._javascriptChannels?.controller = _innercontroller;
widget._urlIntercept?.controller = _innercontroller;
//本地与线上文件展示
if(!TextUtil.isNetUrl(widget._url)){
_loadHtmlAssets(controller);
}else{
controller.loadUrl(widget._url);
}
},
onPageFinished: (url) async{
//加载完成
LibLoading.dismiss();
ULog.d("${url} loading finish");
_controller.runJavascriptReturningResult("document.title").then((result){
widget._titleCallBack?.call(result);
});
},
onPageStarted: (String url) {
ULog.d("${url} loading start");
},
onWebResourceError: (error){
//错误回调
LibLoading.dismiss();
ULog.d("loading error -> ${error.errorCode},${error.description},${error.domain},${error.errorType},${error.failingUrl}");
widget._onWebResourceError?.call(error);
},
navigationDelegate : (NavigationRequest request){
//拦截处理
if (widget._urlIntercept?.baseUrlIntercept(request.url)??false) {
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
// initialUrl : TextUtil.isNetUrl(widget._url)?widget._url:Uri.dataFromString(widget._url, mimeType: 'text/html', encoding: Encoding.getByName('utf-8')).toString(),
// 是否支持js,默认是不支持的
javascriptMode: JavascriptMode.unrestricted,
gestureNavigationEnabled: true, //启用手势导航
//js 调用 flutter
javascriptChannels: widget._javascriptChannels?.baseJavascriptChannels(context),
);
}
//加载本地文件
_loadHtmlAssets(WebViewController controller) async {
String htmlPath = await DefaultAssetBundle.of(context).loadString(widget._url);
controller.loadUrl(Uri.dataFromString(htmlPath,mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
.toString());
}
}
四、app中的实现与使用
4.1 JavascriptChannels 实现
class WisdomworkJavascriptChannels extends JavascriptChannels{
@override
Set? otherJavascriptChannels(BuildContext context) {
return {_appInfoJavascriptChannel(context),
_reportNameJavascriptChannel(context),
_saveImageJavascriptChannel(context),
};
}
//调用函数
JavascriptChannel _appInfoJavascriptChannel(BuildContext context) {
var jname = 'appInfo';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("showBar")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_BAR,user["showBar"]!);
// setState(() {
// isShowToolBar = user["showBar"]!;
// });
}
if(user.containsKey("shareFlag")){
webPageCallBack?.call(LibWebPage.ACTION_SHOW_RIGHT,user["shareFlag"]!);
// setState(() {
// hasShare = user["shareFlag"]!;
// });
}
// 数据传输
String callbackname = message.message; //实际应用中要通过map通过key获取
Map backParams = {
"userToken": UserStore().getUserToken()??"",
"userId": UserStore().getUserId()??"",
"userName": UserStore().getUserName()??"",
"titleHeight":MediaQuery.of(context).size.height * 0.07,
"statusHeight":MediaQueryData.fromWindow(window).padding.top,
"role": "teacher"
};
String jsondata= convert.json.encode(backParams);
controller?.runJavascript("callJS", [jsondata],true);
});
}
JavascriptChannel _reportNameJavascriptChannel(BuildContext context) {
var jname = 'getReportName';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("reportName")){
webPageCallBack?.call(WisdomworkLibWebPageCallback.REPORT_NAME,user['reportName']!);
}
});
}
JavascriptChannel _saveImageJavascriptChannel(BuildContext context) {
var jname = 'savePicture';
return JavascriptChannel(
name: jname,
onMessageReceived: (JavascriptMessage message) {
logFunctionName(jname,message.message);
Map user = convert.jsonDecode(message.message);
if(user.containsKey("url")){
var url = user['url']!;
if(url.isNotEmpty){
ULog.d("下载的地址:$url");
ImageTool.saveImageToPhoto(url);
}
}
});
}
}
,>,>,>,>
4.2 UrlIntercept 实现
class WisdomworkUrlIntercept extends UrlIntercept{
WisdomworkUrlIntercept() : super((String url,InnerWebPageController? _controller) {
return false;
});
}
4.3 web通用页实现
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:flutter_base_ui/src/widget/appbar/default_app_bar.dart';
import 'package:flutter_base_ui/src/widget/web/inner_web_page.dart';
import 'package:flutter_base_ui/src/widget/web/url_intercept.dart';
import 'javascript_channels.dart';
abstract class LibWebPageCallBack{
void libWebPagerightBtn(String? key,dynamic value,InnerWebPageController _controller);
void libWebPageCallBack(String? key,dynamic value,InnerWebPageController _controller);
}
class LibWebPage extends StatefulWidget{
static const String TITLE = "title";
static const String URL = "url";
static const String RIGHT = "right";
static const String RIGHT_VALUE = "rightValue";
static const String RIGHT_KEY = "rightKey";
static const String BACKPAGE = "backpage";
static const String ACTION_SHOW_BAR = "actionShowBar";
static const String ACTION_BACK = "actionBack";
static const String ACTION_SHOW_RIGHT = "actionShowRight";
static LibWebPage start(Map argument,{JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,LibWebPageCallBack? libWebPageCallBack,Widget? back}){
return LibWebPage(argument[URL]!,title: argument[TITLE],javascriptChannels: javascriptChannels,urlIntercept: urlIntercept,back:back,libWebPageCallBack: libWebPageCallBack
,backPage: argument[BACKPAGE],right: argument[RIGHT],rightValue: argument[RIGHT_VALUE],rightkey: argument[RIGHT_KEY],isShowRight: argument[ACTION_SHOW_RIGHT],isShowToolBar: argument[ACTION_SHOW_BAR],);
}
static Map getArgument(String url,{String? title,bool? backPage, Widget? right,bool? isShowToolBar,bool? isShowRight,String? rightkey,dynamic rightValue}){
return {
URL :url,
TITLE :title,
RIGHT :right,
RIGHT_VALUE :rightValue,
RIGHT_KEY :rightkey,
BACKPAGE :backPage,
ACTION_SHOW_BAR :isShowToolBar,
ACTION_SHOW_RIGHT :isShowRight,
};
}
String? title;
final String url;
JavascriptChannels? _javascriptChannels;
UrlIntercept? _urlIntercept;
LibWebPageCallBack? _libWebPageCallBack;
bool _backPage;
Widget? _right;
String? _rightkey;
dynamic _rightValue;
Widget? _back;
bool isShowToolBar;
bool isShowRight;
LibWebPage(String url,{String? title,bool? isShowToolBar,bool? isShowRight,JavascriptChannels? javascriptChannels,UrlIntercept? urlIntercept,Widget? back,bool? backPage, Widget? right,String? rightkey,dynamic rightValue,LibWebPageCallBack? libWebPageCallBack})
:this.url = url,_javascriptChannels = javascriptChannels,_urlIntercept = urlIntercept,this.title = title,this._back = back,this.isShowToolBar = isShowToolBar??true,this.isShowRight = isShowRight??true,
_backPage = backPage??false,_right = right,_rightkey = rightkey,_rightValue = rightValue,_libWebPageCallBack = libWebPageCallBack;
@override
State createState() => _LibWebPageState();
}
class _LibWebPageState extends State{
late InnerWebPageController _innerWebPageController;
String? urlTitle;
// EmptyStatusController? emptyStatusController;
var status = EmptyStatus.none;
WebPageCallBack? webPageCallBack;
@override
void initState() {
super.initState();
webPageCallBack = (String name,dynamic value){
widget._libWebPageCallBack?.libWebPageCallBack(name, value, _innerWebPageController);
switch(name){
case LibWebPage.ACTION_SHOW_BAR:
setState(() {
widget.isShowToolBar = value;
});
break;
case LibWebPage.ACTION_SHOW_RIGHT:
setState(() {
widget.isShowRight = value;
});
break;
case LibWebPage.ACTION_BACK:
if(value){
Navigator.of(context).pop();
}else{
_goBack(context).then((value) => {
Navigator.of(context).pop()
});
}
break;
}
};
}
@override
Widget build(BuildContext context) {
var title;
if(widget.title == null){
if(urlTitle!=null){
title = urlTitle;
}
}else{
title = widget.title;
}
return WillPopScope(child: Scaffold(
appBar: !widget.isShowToolBar? null
: DefalutBackAppBar(title??"",back : widget._back,showRight :widget.isShowRight,tap: () => _goBack(context),right: widget._right,rightcallback: (){
widget._libWebPageCallBack?.libWebPagerightBtn(widget._rightkey, widget._rightValue, _innerWebPageController);
},),
body: LibEmptyView(
layoutType: status,
refresh: () {
status = EmptyStatus.none;
_innerWebPageController.reload();
},
child: InnerWebPage(widget.url,titleCallBack: (title){
setState(() {
urlTitle = title;
});
},javascriptChannels: widget._javascriptChannels,urlIntercept: widget._urlIntercept,onInnerWebPageCreated: (innerWebPageController){
_innerWebPageController = innerWebPageController;
widget._javascriptChannels?.webPageCallBack = webPageCallBack;
widget._urlIntercept?.webPageCallBack = webPageCallBack;
},onWebResourceError: (error){
setState(() {
status = EmptyStatus.fail;
});
},),
),
),
onWillPop: () {
return _goBack(context);
});
}
Future _goBack(BuildContext context) async {
if(widget._backPage){
return true;
}
if (await _innerWebPageController.canGoBack()) {
_innerWebPageController.goBack();
return false;
}
return true;
}
}
,>,>
4.3 外部页面WebPageCallBack 回调,处理js交互逻辑
例子(处理下载pdf,并分享)
class WisdomworkLibWebPageCallback extends LibWebPageCallBack{
static const String REPORT_DETAIL = "ReportDetail";
static const String REPORT_NAME = "reportName";
String? reportName;
@override
void libWebPageCallBack(String? key, dynamic value, InnerWebPageController _controller) {
switch(key){
case REPORT_NAME:
reportName = value;
break;
}
}
@override
void libWebPagerightBtn(String? key, dynamic value, InnerWebPageController _controller) {
switch(key){
case REPORT_DETAIL:
if(reportName?.isEmpty??true){
TipToast.instance.tip("网页加载完毕后再分享");
return;
}
LibLoading.show(status: "下载中");
ReportResponsitory.instance.createFileOfPdfUrl(value.toString(),reportName!).then((f) {
ULog.d(f);
String pdfpath = f.path;
List imagePaths = [];
imagePaths.add(pdfpath);
final box = LibRouteNavigatorObserver.instance.navigator!.context.findRenderObject() as RenderBox?;
LibLoading.dismiss();
Share.shareFiles(imagePaths,
mimeTypes: ["application/pdf"],
text: null,
subject: null,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
});
break;
}
}
}
以上就是flutter 的Hybrid 混合开发封装
本人将js与拦截操作从原有的web组件中抽离出来,相当于业务抽离在外。与webview的耦合降低。
感谢大家阅读我的文章