跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview

前言

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();
  }

}

三、构建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的耦合降低。
感谢大家阅读我的文章

你可能感兴趣的:(跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview)