flutter与native的交互详解

背景

flutter依托Skia的深度定制,给我们提供了很多关于渲染的支持。能够实现跨平台应用层的渲染的一致性。但是我们在实际的应用开发中,除了基本的UI展示,还有更多的功能逻辑需要使用系统的底层的能力,比如:推送、拍照等功能。由于flutter只接管应用渲染层,因此这些系统的底层能力无法在flutter框架内提供支持。另一方面flutter问世的是时间相对较短,一些比较成熟的解决方案在flutter中还没有相关的实现方案,所以和原生交互能力是至关重要的。

应用开发

基于背景部分的思考,我们明确了与原生交互能解决flutter调用原生能力的需求场景,那底层能力 + 应用层渲染,就可以搞定一个App的所有场景吗?在下结论之前,我们先按照四象限分析法,把能力和渲染分解成四个维度,分析构建一个相对完整的 App 需要什么

四象限分析法.png

如图中所示,开发一个 App 需要覆盖的知识点其实是非常多的,flutter和原生交互只能搞定应用层渲染、应用层能力和底层能力,对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,我们重新在flutter 上重新开发一套显然不太现实。在这种情况下,使用混合视图的场景需求就显而易见。我们可以在 flutter 的 Widget 树中提前预留一块空白区域,在 flutter 的画板中(即 FlutterView 与 FlutterViewController)嵌入一个与空白区域完全匹配的原生视图,就可以实现想要的视觉效果了。但是,采用这种方案极其不优雅,因为嵌入的原生视图并不在 flutter 的渲染层级中,需要同时在 flutter 侧与原生侧做大量的适配工作,才能实现正常的用户交互体验。那么针对这些问题,我们接下来看看flutter给开发者都提供了哪些支持。

相关支持

  • Method Channel

为了解决调用原生系统底层能力以及相关代码库复用问题,flutter提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给 Dart,从而实现 Dart 代码与原生代码的交互,就像调用了一个普通的 Dart API 一样。

  • Platform View

flutter 提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在 flutter 里面嵌入原生系统(Android 和 iOS)的视图,并加入到 flutter 的渲染树中,实现与 flutter 一致的交互体验。这样一来,通过平台视图,我们就可以将一个原生控件包装成 flutter 控件,嵌入到 flutter 页面中,就像使用一个普通的 Widget 一样。

下面我们详细探究一下flutter的这两个解决方案,两个方案的实现原理,方案是否如上所述能解决我们实际开发中的问题。

Method Channel 方法通道

flutter 作为一个跨平台框架,提供了一套标准化的解决方案,为开发者屏蔽了操作系统的差异。但,flutter 毕竟不是操作系统,因此在某些特定场景下(比如推送、蓝牙、摄像头硬件调用时),也需要具备直接访问系统底层原生代码的能力。为此,flutter 提供了一套灵活而轻量级的机制来实现 Dart 和原生代码之间的通信,即方法调用的消息传递机制,而方法通道则是用来传递通信消息的信道。 一次典型的方法调用过程类似网络调用,由作为客户端的 flutter,通过方法通道向作为服务端的原生代码宿主发送方法调用请求,原生代码宿主在监听到方法调用的消息后,调用平台相关的 API 来处理 flutter 发起的请求,最后将处理完毕的结果通过方法通道回发至 flutter。调用过程如下图所示:

MethodChannel.png

从上图中可以看到,方法调用请求的处理和响应,在 Android 中是通过 FlutterView,而在 iOS 中则是通过 FlutterViewController 进行注册的。FlutterView 与 FlutterViewController 为 Flutter 应用提供了一个画板,使得构建于 Skia 之上的 Flutter 通过绘制即可实现整个应用所需的视觉效果。因此,它们不仅是 flutter 应用的容器,同时也是 flutter 应用的入口,自然也是注册方法调用请求最合适的地方。接下来,我通过一个例子来演示如何使用方法通道实现与原生代码的交互。

方法通道使用示例

在实际业务中,提示用户跳转到应用市场(iOS 为 App Store、Android 则为各类手机应用市场)去评分是一个高频需求,考虑到 flutter 并未提供这样的接口,而跳转方式在 Android 和 iOS 上各不相同,因此我们需要分别在 Android 和 iOS 上实现这样的功能,并暴露给 Dart 相关的接口。我们先来看看作为客户端的 flutter,怎样实现一次方法调用请求。

flutter 如何实现一次方法调用请求?

首先,我们需要确定一个唯一的字符串标识符,确定一个命名通道;在这个通道之上,flutter 通过指定方法名“fcredirect://personal_friends_takeover”来发起一次方法调用请求。可以看到,这和我们平时调用一个 Dart 对象的方法完全一样。因为方法调用过程是异步的,所以我们需要使用非阻塞(或者注册回调)来等待原生代码给予响应。 下面展示一下时间一个简单的分享功能的flutter和原生交互的部分代码

// 声明MethodChannel
const methodChannel = const MethodChannel('flutter_native');
_iOSPushToFriend() async {
   await methodChannel.invokeMethod('redirect://personal_friends_takeover');
   // 需要注意的是,方法调用请求有可能会失败,实际开发中我们需要把发起方法调用请求的语句用 try-catch 包装起来。
}
复制代码

在 iOS 平台,方法调用的处理和响应是在 flutter 应用的入口,即 FlutterViewController里实现的,我们可以在宿主App中做如下的实现:

func flutterMethodChannel(viewController: FlutterViewController) {
    let channelName = "flutter_native"
    let methodChannel = FlutterMethodChannel.init(name: channelName, binaryMessenger: viewController.binaryMessenger)
        
    methodChannel.setMethodCallHandler {(call: FlutterMethodCall, result: @escaping FlutterResult) in
        if (call.method == "redirect://personal_friends_takeover") {
            print("进行交互")
        } else {
            print("nothing")
        }
    }
}
复制代码

需要注意的是,涉及到跨系统数据交互,flutter 会使用 StandardMessageCodec 对通道中传输的信息进行类似 JSON 的二进制序列化,以标准化数据传输行为。这样在我们发送或者接收数据时,这些数据就会根据各自系统预定的规则自动进行序列化和反序列化。关于 Android、iOS 和 Dart 平台间的常见数据类型转换,总结成了下面一张表格,帮助你理解与记忆。你只要记住,像 null、布尔、整型、字符串、数组和字典这些基本类型,是可以在各个平台之间以平台定义的规则去混用的,就可以了。

平台间的数据类型转换.png

Platform View(平台视图)

如果说方法通道解决的是原生能力逻辑复用问题,那么平台视图解决的就是原生视图复用问题。flutter 提供了一种轻量级的方法,让我们可以创建原生(Android 和 iOS)的视图,通过一些简单的 Dart 层接口封装之后,就可以将它插入 Widget 树中,实现原生视图与 flutter 视图的混用。一次典型的平台视图使用过程与方法通道类似:

  • 首先,由作为客户端的 flutter,通过向原生视图的 flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
  • 然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
  • 最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。

至此,我们就可以像使用 Widget 那样,使用原生视图了。整个流程,如下图所示:

PlatformView.png

flutter 如何实现原生视图的接口调用?

在下面的代码中,我们在 SampleView 的内部,分别使用了原生 Android、iOS 视图的封装类 AndroidView 和 UIkitView,并传入了一个唯一标识符,用于和原生视图建立关联:

class SampleView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //使用Android平台的AndroidView,传入唯一标识符sampleView
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(viewType: 'sampleView');
    } else {
      //使用iOS平台的UIKitView,传入唯一标识符sampleView
      return UiKitView(viewType: 'sampleView');
    }
  }
}
复制代码

可以看到,平台视图在 flutter 侧的使用方式比较简单,与普通 Widget 并无明显区别。 平台视图解决了原生渲染能力的复用问题,使得 flutter 能够通过轻量级的代码封装,把原生视图组装成一个 flutter 控件。flutter 提供了平台视图工厂和视图标识符两个概念,因此 Dart 层发起的视图创建请求可以通过标识符直接找到对应的视图创建工厂,从而实现原生视图与 flutter 视图的融合复用。对于需要在运行期动态调用原生视图接口的需求,我们可以在原生视图的封装类中注册方法通道,实现精确控制原生视图展示的效果。

总结

方法通道

方法通道解决了逻辑层的原生能力复用问题,使得 flutter 能够通过轻量级的异步方法调用,实现与原生代码的交互。一次典型的调用过程由 flutter 发起方法调用请求开始,请求经由唯一标识符指定的方法通道到达原生代码宿主,而原生代码宿主则通过注册对应方法实现、响应并处理调用请求,最后将执行结果通过消息通道,回传至 flutter。

平台视图

由于 flutter 与原生渲染方式完全不同,因此转换不同的渲染数据会有较大的性能开销。如果在一个界面上同时实例化多个原生控件,就会对性能造成非常大的影响,所以我们要避免在使用 flutter 控件也能实现的情况下去使用内嵌平台视图。因为这样做,一方面需要分别在 Android 和 iOS 端写大量的适配桥接代码,违背了跨平台技术的本意,也增加了后续的维护成本;另一方面毕竟除去地图、WebView、相机等涉及底层方案的特殊情况外,大部分原生代码能够实现的 UI 效果,完全可以用 flutter 实现。

猜你喜欢

转载自juejin.im/post/7041113733979963429