Pano Flutter SDK 设计经验与实践浅谈

Flutter 是谷歌推出的移动 UI 框架,能够快速在 iOS 和 Android 上构建高质量的原生用户界面,被愈来愈多的开发者选择和使用。拍乐云也提供了功能强大的 Pano Flutter SDK,性能稳定且易用,覆盖语音通话、视频通话、互动白板、互动直播、云端录制等各类功能。在以前的一篇《Pano Flutter SDK 全新发布》中,咱们给你们介绍了SDK的详细接入流程,今天将继续聊聊咱们 Pano Flutter SDK 的设计思路与实践经验。java

#1git

整体结构github

Pano 针对原生应用开发提供了完备的高性能SDK,因此 Pano Flutter SDK 采用插件包的形式来封装咱们的SDK。相似的,在 RN 中咱们采用 NativeModule 来实现 Pano RN SDK。Pano 移动端跨平台 SDK 的整体结构以下图所示:缓存

整体框架.png

SDK 分为三层结构,底层为 Pano 原生 SDK(iOS&Android)。基于原生SDK 之上为桥接层,因为 Flutter 与 RN 中与原生层通讯均为异步通讯,且需使用特定的通讯方式(Flutter 使用平台通道方案,RN 则使用原生模块方案),因此须要将跨平台调用进行转换才能调用原生 SDK 方法。所以桥接层将分为两个部分,原生 SDK 桥接与跨平台(Flutter&RN)桥接,以达到最大化代码复用的目的,将原生 SDK 接口二次封装成通用异步接口,在其上分别对接 Flutter 和 RN 的通讯接口。SDK 最顶层则为跨平台层,对接原生层通讯接口封装出 Flutter 或 RN 平台的功能接口。markdown

虽然最终的结构比较简洁明了,可是因为 Flutter 或 RN 对于视图更新机制与原生开发存在较大差别,以及跨平台层与原生层数据结构的不一样等问题,致使 SDK 的设计与实现中存在许多涉及数据转换、对象映射、内存管理等难点或坑点,接下来将结合 SDK 的设计思路与实践经验,针对其中几个典型的问题谈谈解决方案或须要注意的地方。数据结构

#2框架

工做流程异步

Pano Flutter SDK 提供的 API 基本上与原生 SDK 保持一一对应的关系,以便开发人员能够轻松的将对接原生 SDK 开发经验应用到 Flutter 中。但因为 Flutter 特殊的平台通道(Platform Channel)方案以及视图更新机制,因此并非简单的将原生 SDK 接口进行透传封装,SDK 的调用流程以下图所示:ide

工做流程.png

SDK 使用 Flutter 平台通道中 MethodChannel 与 EventChannel 来实现Flutter 层与原生层通讯,其中 MethodChannel 用于 Flutter 向原生层方法调用,EventChannel 则用于原生层向 Flutter 层进行数据流通讯,这里主要是传递原生层回调消息。当开发者调用 Flutter 层接口,SDK 使用对应的 MethodChannel 将方法名、参数传递到原生层,SDK 在这里实现了 Flutter Native Bridge 来专门处理这些调用。工具

建议:当原生层接收到MethodChannel的方法调用时(例如:iOS为-[FlutterPlugin handleMethodCall:result:]),采用反射调用(例如:iOS中使用NSSelectorFromString获取selector,而后经过[NSObject performSelector:withObject:]调用)Native SDK Bridge方法,这样能够尽可能将Flutter的逻辑与原生桥接层逻辑隔离,一方面作薄对接Flutter层逻辑,另外一方面将须要常常跟随原生SDK变更的原生桥接层逻辑与其它跨平台框架(如RN)进行复用,减小维护成本。 注意:Flutter 中没有现成的二进制数据类型,一般采用Uint8List来代替,但经过平台通道转换后,在iOS端会转换成FlutterStandardTypedData类型,该类型不能自动转换为NSData类型,须要经过其属性data来获取实际的NSData对象。但在从原生层调用Flutter层时,能够直接传递NSData对象,其将会在 Flutter 层被自动转换为Uint8List。 Flutter 中平台通道其实是将传递的数据编码成消息的形式,跨线程发送到该应用所在的宿主原生层。而且 Native SDK Bridge 对接原生 SDK,将原生 SDK 方法实行完毕后的返回值经过 callback 返回时,也是将数据编码成消息经过一样方式原路返回给 Flutter 层。整个过程的消息和响应是异步的,这也就是 Flutter 层接口都设计成异步接口的缘由。

注意:MethodChannel类型中,调用原生方法使用Future<T?> invokeMethod(String method, [ dynamic arguments ]),对于SDK返回Flutter支持的基本类型数据时,直接调用没有任何问题,例如当获取SDK版本号接口返回String类型,则Flutter层接口能够实现为:static Future getSdkVersion() { // iOS中NSString和Andorid中java.lang.String均可以自动转换为Flutter的String类型 return _methodChannel.invokeMethod('getSdkVersion'); } 但当返回非基本类型时,返回值就须要进行转换,例如开启音频接口因为可能有多种结果,因此返回值是枚举类型ResultCode,若是直接按照如下写法实现将会报错:Future startAudio() { return _methodChannel.invokeMethod('startAudio');// 错误:返回值为int不会自动转换Flutter的枚举类型 } 须要增长转换逻辑,例如:Future startAudio() { return _methodChannel.invokeMethod('startAudio').then((value) { return ResultCodeConverter.fromValue(value).e as T; // ResultCodeConverter为将int转换ResultCode的工具类 }); } 建议:因为 SDK 中存在大量的返回 ResultCode 的方法,在每一个接口实现处都增长转换代码繁琐且冗余,因此咱们对于这种状况能够提取一个公共模板方法,能很大程度提高代码简洁度,例如:Future_invokeMethod(String method, [Map<String, dynamic> arguments]) { if (T == ResultCode) { // 判断当前范型为ResultCode时,增长转换逻辑 return _methodChannel.invokeMethod(method, arguments).then((value) { return ResultCodeConverter.fromValue(value).e as T; }); } else { // 其余能够自动转换的状况则返回调用结果 return _methodChannel.invokeMethod(method, arguments); } } 以上是 Flutter 调用原生层的流程,那当原生层须要回调事件给 Flutter层咱们应该怎么作呢?这时就须要利用 EventChannel 来实现。先看下EventChannel 的基本流程:

原生层调用 setStreamHandler(iOS为-[FlutterEventChannel setStreamHandler:])注册 Handler 实现; EventChannel 初始化完成后,经过StreamHandler的onListen(iOS为-[FlutterStreamHandler onListenWithArguments:eventSink:])回调接口获取eventSink引用并保存; Flutter 层调用 EventChannel 的 receiveBroadcastStream 注册监听; 原生层经过调用 eventSink 发送事件消息。 建议:EventChannel 因为是数据流通讯,跟 MethodChannel 不一样之处在于没有封装出针对方法回调的模型,但目前 SDK 中原生层向Flutter 层均为方法回调,因此咱们将回调数据组装成特定格式的键值对,如:{ "methodName": xxxx, // 回调方法名 "data": [xxxx,xxxx...] // 回调参数列表 } 而后在 Flutter 层进行统一解析处理:void setEventHandler(RtcEngineEventHandler handler) { _handler = handler; ... _eventChannel.receiveBroadcastStream().listen((event) { final eventMap = Map<dynamic, dynamic>.from(event); final methodName = eventMap['methodName'] as String; final data = List.from(eventMap['data']); _handler?.process(methodName, data); }); } 至此经过以上方案,已经能够封装原生 SDK 的绝大部分功能以 Flutter SDK 形式提供出去了。但还剩一个重要的问题须要解决,那就是如何设置原生层视图的逻辑。

#3

设置原生视图

因为 Flutter 提供的平台通道方案本质上是以字节流的方式在线程间传递数据,因此对于原生层视图等非序列化的对象是不支持的。Flutter 在如何内嵌原生层视图的问题上,提供了平台视图(Platform-views)方案,开发者能够在 Flutter 层建立原生视图的映射(iOS为UiKitView,Android 为 AndroidView),并嵌入到 Widget 中。

那如何将生成的原生视图对象传递给原生层 SDK?在 Flutter 建立原生视图后,会返回视图对应惟一的 id,因此最直观的方法就是在 id 返回后,分别在原生层与 Flutter 层生成对应的 MethodChannel,组成键值对缓存起来,在调用时经过 id 查找 MethodChannel,而后经过 MethodChannel 传递方法调用消息。但这样作有两个明显缺陷:

MethodChannel 没有与 Widget 直接关联,在 Widget 销毁时须要手动清除键值对中的 MethodChannel; 采用 id 做为原生视图的标识,因为缺乏有效性检查,可能致使调用到无效 MethodChannel 抛出异常。 而且一般原生SDK方法中是须要原生视图做为参数传入,但因为只能经过与视图对应的MethodChannel才能在原生层访问到对应的原生视图对象,致使无法直接在Flutter层设计出相似原生SDK的方法。

建议:Pano Flutter SDK中咱们为了尽可能保持与原生SDK的接口一致性,采起了一种曲线救国的方案。在建立渲染视图RtcSurfaceView(StatefulWidget)后,回调返回保存了MethodChannel的ViewModel对象: class RtcSurfaceViewModel { final MethodChannel _methodChannel;

Future invokeMethod(String method, [Map<String, dynamic> arguments]) { if (T == ResultCode) { return _methodChannel.invokeMethod(method, arguments).then((value) { return ResultCodeConverter.fromValue(value).e as T; }); } else { return _methodChannel.invokeMethod(method, arguments); } }

RtcSurfaceViewModel(this._methodChannel); } 而后按照须要原生视图的SDK方法,定义出对应的Flutter层接口,接收ViewModel做为参数,方法实现调用ViewModel的MethodChannel传递方法消息,例如开启视频时调用startVideo接口定义以下:Future startVideo(RtcSurfaceViewModel viewModel, {RtcRenderConfig config}) { config ??= RtcRenderConfig(); return viewModel.invokeMethod('startVideo', {'config': config.toJson()}); } 在原生层视图对应的MethodChannel接收到方法调用,经过原生层内部缓存的engine对象,调用对应的SDK方法(如startVideo),传入原生层视图完成接口调用。这样作,一方面让MethodChannel与Widget关联,另外一方面在接口调用上也使用ViewModel对象保证了传值的有效性。而且接口上也基本与原生SDK保持了一致性,下降了对接SDK的开发人员的理解成本,也兼顾了代码的维护成本。 #4

结语

现今广大开发每每会遇到各类各样跨平台开发的需求或问题,而拍乐云一直以来坚持以开发者为先,和用户在一块儿。Pano Flutter SDK 所有开源,你能够经过 GitHub(github.com/PanoVideo/P… )查看完整源码。经过本篇介绍 Pano Flutter SDK 的跨平台 SDK 设计与实践经验,但愿能给你们带来一些帮助与启发。

相关文章
相关标签/搜索