Flutter WebView使用以及分析

一、背景

在开发过程中很多时候都需要用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能力,降低了实现成本。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • WebView 的使用,算得上是比较普遍的,特别是与 JS 的交互,今天整理一下在 flutter 中使用 Web...
    yanftch阅读 20,338评论 11 17
  • 目录 前言 webview_flutter 官方组件 flutter_webview 社区组件 JS调用Flutt...
    Yue_Q阅读 11,025评论 5 7
  • lzyprime 博客 (github) 创建时间:2020.03.06qq及邮箱:2383518170 λ...
    lzyprime阅读 1,819评论 0 1
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,202评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,033评论 0 4