美文网首页Flutter
Flutter WebView使用以及分析

Flutter WebView使用以及分析

作者: 林军华 | 来源:发表于2021-07-25 17:52 被阅读0次

    一、背景

    在开发过程中很多时候都需要用WebView展示网页,在android中可以直接使用WebView控件加载网页,iOS也有WKWebView或UIWebView,那么在flutter中如何加载网页?

    从以下问题入手:

    • flutter中是否有类似原生的WebView控件?
    • flutter中如何使用WebView加载网页?
    • flutter的WebView如何与js通信的?
    • flutter中WebView是如何实现?

    二、Flutter WebView

    flutter层不支持webview,加载网页的功能还需要借助原生控件来处理。

    由于Webview是一个非常复杂的控件,flutter再重新实现一遍成本非常高,而各个平台都有很完善的WebView控件,故flutter团队提供了嵌入原生WebView的解决方案,flutter通过 PlatformView 使用原生控件。

    通过pub.dev搜索以及对比网上文章,发现了几个比较受欢迎的flutter webview插件,

    webview库排行.png

    三个插件对比:

    flutterwebview对比.png

    对比总结:

    • flutter_inappwebview:功能非常丰富,文档非常完善,属于三方库中的精品,推荐使用
    • webview_flutter:功能一般,满足基本功能需求,官方出品持续完善中。
    • flutter_webview_plugin:功能不够完善,现有功能将积极合入webview_flutter,后续不在维护,不建议使用。

    三、使用

    下文将使用flutter_inappwebview,进行使用简介,文档地址

    flutter_inappwebview主要功能清单如下:

    • InAppWebView:Flutter Widget,用于添加集成到 Flutter 小部件树中的内联原生 WebView。
    • ContextMenu:此类表示 WebView 上下文菜单。
    • HeadlessInAppWebView:表示无头模式下的 WebView 的类。它可用于在后台运行 WebView,而无需将 InAppWebView 附加到小部件树。
    • InAppBrowser:使用本机 WebView 的应用内浏览器。
    • ChromeSafariBrowser:在 Android / SFSafariViewController 上使用 Chrome 自定义标签的应用程序内浏览器。
    • InAppLocalhostServer:这个类允许你在 http://localhost:[port]/ 上创建一个简单的服务器。默认端口值为 8080。
    • CookieManager:此类实现了一个单例对象(共享实例),该对象管理 WebView 实例使用的 cookie。
    • HttpAuthCredentialDatabase:此类实现管理共享 HTTP 身份验证凭据缓存的单例对象(共享实例)。
    • WebStorageManager:这个类实现了一个单例对象(共享实例),它管理 WebView 实例使用的 Web 存储。

    可以看出,flutter_inappwebview支撑多种WebView的使用方式,下文以 InAppWebView 为例进行介绍,因为InAppWebView是Flutter组件可以嵌入Widget Tree,用法更灵活,更贴近实际需求。

    Flutter 支持两种模式集成原生控制:虚拟显示模式 (Virtual displays) 和混合集成模式 (Hybrid composition) :

    • Virtual displays:虚拟显示模式会将 android.view.View 实例渲染为纹理,因此它不会嵌入到 Android Activity 的视图层次结构中。某些平台交互(例如键盘处理和辅助功能)可能无法正常工作。
    • Hybrid composition:混合集成模式需要 Flutter 1.22 (推荐使用 1.22.2 版本)。这种模式将原生的 android.view.View 附加到视图层次结构中。因此,键盘处理和无障碍功能是开箱即用的。在 Android 10 之前,此模式可能会大大降低 Flutter UI 的帧吞吐量 (FPS)。

    根据具体情况来决定使用哪种模式。flutter webview在iOS平台仅支持Hybrid Composition 模式。

    推荐使用Hybrid Composition 模式。

    3.1 引入依赖

    flutter项目配置文件pubspec.yaml中引入依赖:

    dependencies:  
      flutter_inappwebview: ^5.3.2
    

    android项目配置:

    android {
        compileSdkVersion 29
        
        defaultConfig {
            minSdkVersion 19
            targetSdkVersion 29
        }
    }
    

    gradle相应版本:

    gradle插件版本 :4.1.* 以及上

    gradle版本 :5.6 及以上

    注意事项:

    flutter_inappwebview需要依赖androidx和swift。

    3.2 flutter中使用

    widget中使用,并开启Hybrid Composition模式。

    import 'package:flutter/material.dart';
    import 'package:flutter_inappwebview/flutter_inappwebview.dart';
    
    class Browser extends StatefulWidget {
      const Browser(Key key, this.url, this.title) : super(key: key);
    
      final String url;
      final String title;
    
      @override
      _BrowserState createState() => _BrowserState();
    }
    
    class _BrowserState extends State<Browser> {
      final GlobalKey webViewKey = GlobalKey();
      InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
        crossPlatform: InAppWebViewOptions(
          useShouldOverrideUrlLoading: true,
          mediaPlaybackRequiresUserGesture: false,
        ),
        /// android 支持HybridComposition
        android: AndroidInAppWebViewOptions(
          useHybridComposition: true,
        ),
        ios: IOSInAppWebViewOptions(
          allowsInlineMediaPlayback: true,
        ),
      );
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: InAppWebView(
            key: webViewKey,
            initialUrlRequest: URLRequest(url: Uri.parse(widget.url)),
            initialOptions: options,
          ),
        );
      }
    }
    

    如果只加载网页不要复杂的定制,这样的配置已经可以啦~

    3.3 InAppWebview提供的接口

    InAppWebview实现了原生WebView(Android)和WKWebView(iOS)中绝大多数接口,将这些原生接口代理到了Flutter层,极大的提高了不同平台的使用效率。

    接口组织方式分为两部分:

    • 接口配置:控制接口功能是否启用。
    • 接口回调:实现接口来处理业务逻辑。

    在使用某个接口功能时,需要先检测相应接口开关是否开启,开启有对应接口回调才会生效,一些常见的生命周期回调函数则不需要配置开关。

    因为不同平台本身的差异,整体接口分为三类,通过InAppWebViewGroupOptions类进行配置管理:

    • crossPlatform: InAppWebViewOptions 用于配置Android和iOS通用的接口功能。
    • android: AndroidInAppWebViewOptions 仅用于配置Android特有的接口功能。
    • ios: IOSInAppWebViewOptions 仅用于配置iOS特有的接口功能。

    接口配置实例:

    InAppWebViewGroupOptions options = InAppWebViewGroupOptions(
        //夸平台配置
        crossPlatform: InAppWebViewOptions(
          useShouldOverrideUrlLoading: true,//加载url拦截功能
          useShouldInterceptAjaxRequest: true,//ajax请求拦截
          useOnLoadResource: true,//资源加载回调
          allowFileAccessFromFileURLs: true,//资源加载
          mediaPlaybackRequiresUserGesture: false,//多媒体控制
        ),
        //Android平台配置
        android: AndroidInAppWebViewOptions(
          useHybridComposition: true,//支持HybridComposition
          useShouldInterceptRequest: true,//请求加载链接,可以用于实现Web离线包
        ),
        //iOS平台配置
        ios: IOSInAppWebViewOptions(
          allowsInlineMediaPlayback: true,
        ),
      );
      
      // 使用options配置
      InAppWebView(
            key: webViewKey,
            initialUrlRequest: URLRequest(url: Uri.parse("https://www.baidu.com/")),
            initialOptions: options,//配置options
            // ...
            )
    

    接口回调实例:

    InAppWebView(
       key: webViewKey,
       initialUrlRequest: URLRequest(url: Uri.parse("https://www.baidu.com/")),
       initialOptions: options,// 开关配置项
       onWebViewCreated: (InAppWebViewController controller) {
          print("$TAG onWebViewCreated");
       },
       onLoadStart: (InAppWebViewController controller, Uri? url) {
          print("$TAG onLoadStart url:$url");
       },
       onLoadStop: (InAppWebViewController controller, Uri? url) {
          print("$TAG onLoadStop url:$url");
       },
       onLoadError: (InAppWebViewController controller, Uri? url, int code,
                String message) {
          print("$TAG onLoadError url:$url code:$code message:$message");
       },
       onLoadHttpError: (InAppWebViewController controller, Uri? url,
                int statusCode, String description) {
          print("$TAG onLoadHttpError url:$url statusCode:$statusCode                                   description:$description");
       },
       onConsoleMessage:
            (InAppWebViewController controller, ConsoleMessage consoleMessage) {
              print("$TAG onConsoleMessage consoleMessage:$consoleMessage");
       },
       onProgressChanged: (InAppWebViewController controller, int progress) {
          print("$TAG onProgressChanged progress:$progress");
       },
      shouldOverrideUrlLoading: (InAppWebViewController controller,
                NavigationAction navigationAction) async {
          print("$TAG shouldOverrideUrlLoading navigationAction:$navigationAction");
              return null;
      },
      // 资源加载监听器
      onLoadResource:(InAppWebViewController controller, LoadedResource resource) {
          print("$TAG onLoadResource resource:$resource");
      },
      // 滚动监听器
      onScrollChanged: (InAppWebViewController controller, int x, int y) {
          print("$TAG onScrollChanged x:$x  y:$y");
      },
      onLoadResourceCustomScheme:(InAppWebViewController controller, Uri url) async {
          print("$TAG onLoadResourceCustomScheme url:$url");
          return null;
      },
      onCreateWindow: (InAppWebViewController controller,
                CreateWindowAction createWindowAction) async {
          print("$TAG onCreateWindow");
          return true;
      },
      onCloseWindow: (InAppWebViewController controller) {
          print("$TAG onCloseWindow");
      },
      // 过量滚动监听器
      onOverScrolled: (InAppWebViewController controller, int x, int y,
                bool clampedX, bool clampedY) async {
          print("$TAG onOverScrolled x:$x  y:$y clampedX:$clampedX clampedY:$clampedY");
      },
        
      //Android特有功能,请求加载链接,可以拦截资源加载,并替换为本地Web离线包内的资源
      androidShouldInterceptRequest: (InAppWebViewController controller,
                WebResourceRequest request) async {
          print("$TAG androidShouldInterceptRequest request:$request");
         return null;
      },
        
      //iOS特有功能
      iosOnNavigationResponse: (InAppWebViewController controller,
                IOSWKNavigationResponse navigationResponse) async {
          return null;
      },
    )
    

    接口名称已经可以表示相应功能了,就不一一加注释了~

    3.4 InAppWebview与Js通信

    InAppWebview与Js通信有多种方式,具体文档地址

    a.获取InAppWebViewController

    js通信都是通过 InAppWebViewController 实现的,需要先通过onWebViewCreated方法获取InAppWebViewController 对象:

    // InAppWebview中获取InAppWebViewController
    onWebViewCreated: (InAppWebViewController controller) {
      // ...
    },
    

    b.js调Flutter

    通过InAppWebViewController#addJavaScriptHandler添加js处理器,js的调用将会回调改方法。

    // InAppWebview中获取InAppWebViewController
    onWebViewCreated: (InAppWebViewController controller) {
      // 注册一个JS处理方法,名称为myHandlerName
      controller.addJavaScriptHandler(handlerName: 'myHandlerName', callback: (args) {
        // 打印js方传递过来的参数
        print(args);
    
        // 返回给js方的结果
        return {
          'bar': 'bar_value', 'baz': 'baz_value'
        };
      });
    },
    

    js代码中:

     window.flutter_inappwebview.callHandler('myHandlerName', {a:1,b:2}) 
    

    flutter_inappwebview为InAppWebView框架自动注入的js对象,方便分发js调用到Flutter。

    c.Flutter调js

    通过 InAppWebViewController.evaluateJavascript 调用js方法。在使用之前需要和js放约定好通讯协议:

    • 方法名称
    • 传参数据格式
    • 返回结果数据格式
    onLoadStop: (controller, url) async {
      // 直接调用js代码,并等待结果
      final result = await controller.evaluateJavascript(source: """
        window.handleFlutterInvoke("{a:1,b:2}");
      """);
    
      print("result")
    },
    

    其他方式参考上述文档~

    四、InAppWebview原理

    Flutter的WebView功能是直接依赖不同平台的WebView实现的,已经知道是通过PlatformView实现的,那他们是怎么组合起来的呢?此处以Android平台为例分析下实现过程。

    4.1 Flutter与Android组件关联方式

    主要实现步骤:

    • Android原生实现PlatformView,内部通过getView方法返回具体要用到的Android View

    • Android原生实现PlatformViewFactory ,内部通过create方法创建前一步的PlatformView实例

    • 注册前一步中的平台视图工厂类,并指定viewTypeId用于区分不同的平台视图。

    • FlutterActivity子类中:

    • class MainActivity : FlutterActivity() {
          override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
            flutterEngine
              .platformViewsController
              .registry
              .registerViewFactory("<platform-view-type>", NativeViewFactory())
          }
      }
      
    • 插件中注册:

    • class PlatformViewPlugin : FlutterPlugin {
          override fun onAttachedToEngine(binding: FlutterPluginBinding) {
             binding
              .platformViewRegistry
              .registerViewFactory("<platform-view-type>", NativeViewFactory())
          }
      
          override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
      }
      
    • Flutter层自定义Widget整合Android和iOS层视图

    class CustomFlutterWidget extends StatelessWidget {
     @override
     Widget build(BuildContext context) {
       final defaultTargetPlatform = ...;
         // Android
       if (defaultTargetPlatform == TargetPlatform.android) {
         if (useHybridComposition) {// 混合模式
           return PlatformViewLink(
             viewType: 'viewTypeId',//唯一标识
             // ...
           );
         } else {
           return AndroidView(// 虚拟显示
             viewType: 'viewTypeId',//唯一标识
             // ...
           );
         }
       } else if (defaultTargetPlatform == TargetPlatform.iOS) {
           // iOS
         return UiKitView(// 混合模式
           viewType: 'viewTypeId',//唯一标识
           // ...
         );
       }
       return Text(
           '$defaultTargetPlatform is not yet supported by the flutter_inappwebview plugin');
     }
    }
    

    通过上述步骤就可以使用特定平台的原生View了,iOS也是同样方式实现细节略有差异~

    4.2 InAppWebView实现类图

    FlutterWebView类图.jpg

    整体类图还是比较清晰的,Flutter与平台层通信借助MethodChannel~

    4.3 Flutter与平台层通信

    Flutter提供了多种通信渠道的实现方式,具体文档链接

    • MethodChannel
    • BasicMessageChannel
    PlatformChannels.png
    Flutter MethodChannel
    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    ...
    class _MyHomePageState extends State<MyHomePage> {
      static const platform = const MethodChannel('samples.flutter.io/testMethodChannel');
    
      // 调用通信方法.
      Future<Null> _getBatteryLevel() async {
        try {
          final int result = await platform.invokeMethod('methodName', params);
          print(result);
        } on PlatformException catch (e) {
          
        }
      }
    }
    
    Android MethodChannel
    import android.os.Bundle
    import io.flutter.app.FlutterActivity
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugins.GeneratedPluginRegistrant
    
    class MainActivity() : FlutterActivity() {
      private val CHANNEL = "samples.flutter.io/testMethodChannel"
    
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)
    
        MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
          if(call.methodName == 'methodName') {
              Log.d(TAG,"${call.arguments}")
              // TODO
              result.success(1)
          }                                                       
        }
      }
    }
    

    五、总结

    Q1:flutter中是否有类似原生的WebView控件?

    A1:Flutter没有类似WebView控件,借助平台层实现WebView功能。

    Q2:flutter中如何使用WebView加载网页?

    A2:借助现网提供的WebView插件即可实现网络加载,其中flutter_inappwebview插件非常优秀,推荐使用。

    Q3:flutter的WebView如何与js通信的?

    A3:InAppWebView已经实现了一套完整的js通信机制,如果用官方WebView插件,则需要自己实现一套JsBridge同时适用Android和iOS,成本稍高一点。

    Q4:flutter中WebView是如何实现?

    A4:Flutter本身不提供WebView功能,通过PlatformView去适用各个平台已有的WebView能力,降低了实现成本。

    相关文章

      网友评论

        本文标题:Flutter WebView使用以及分析

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