美文网首页Flutter圈子Flutter中文社区
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hyb

跟我学企业级flutter项目:如何封装一套易用,可扩展的Hyb

作者: 王二蛋和他的狗 | 来源:发表于2022-03-16 20:16 被阅读0次

    前言

    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<void> runJavascript(String funname,List<String>? param,bool brackets) async{
        String javaScriptString = getJavaScriptString(funname,param,brackets);
        await _controller.runJavascript(javaScriptString);
      }
    //带返回值执行网页js,在原有基础上封装,只需要发送jsname与参数
      Future<String> runJavascriptReturningResult(String funname,List<String>? param,bool brackets) async {
        String javaScriptString = getJavaScriptString(funname,param,brackets);
        _controller.runJavascript(javaScriptString);
        return await _controller.runJavascriptReturningResult(javaScriptString);
      }
    //是否可以返回
      Future<bool> canGoBack() {
        return _controller.canGoBack();
      }
    //返回网页历史
      Future<void> goBack() {
        return _controller.goBack();
      }
    //重新加载
      Future<void> reload() {
        LibLoading.show();
        return _controller.reload();
      }
    //获取js请求(工具)
      String getJavaScriptString(String funname, List<String>? param,bool brackets) {
        var strb = StringBuffer(funname);
        if(brackets){
          strb.write("(");
        }
        if(param!=null&&param.length>0){
          for(int i=0;i<param.length;i++){
            strb.write("'${param[i]}'");
            if(i<param.length-1){
              strb.write(",");
            }
          }
        }
        if(brackets){
          strb.write(")");
        }
        ULog.d("JS function -> ${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<JavascriptChannel>? 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<JavascriptChannel>? 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<StatefulWidget> createState() => _InnerWebPageState();
    
    }
    
    class _InnerWebPageState extends State<InnerWebPage> {
    
      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<JavascriptChannel>? 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<String, dynamic > 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<String, dynamic> 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<String, dynamic > 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<String, dynamic > 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<String, dynamic> 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<String, dynamic> 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<StatefulWidget> createState() => _LibWebPageState();
    
    }
    
    class _LibWebPageState extends State<LibWebPage>{
    
      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<bool> _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<String> 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项目:如何封装一套易用,可扩展的Hyb

        本文链接:https://www.haomeiwen.com/subject/cqxmdrtx.html