从零开始|构建 Flutter 多引擎渲染组件:Flutter 工程篇

6,942 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情

承接上文:《从零开始|构建 Flutter 多引擎渲染组件:先导篇》 

前文我们讲了基础环境配置,以及需要提前了解的 Flutter 官方 Demo,但官方 Demo 的工程结构并不可取。

如何更优雅的建设属于 Flutter 多引擎渲染组件的工程结构是十分重要的事,我们希望 Flutter 多引擎渲染组件是十分独立的,可以跟原生 UI 无痕组合,也可以跟任何架构方案不产生冲突,比如 flutter_boost。当然我们也是这样实现的,先展示下我们已经实现了哪一些:

示例.gif

(视频转成 gif,帧数不够所以看起来有些卡顿)

上图中, fgui(FGUI 是内部代号)是任意项目都能使用的通用型组件包,part_home 表明是首页的业务组件包,part_video 是视频编辑器的业务组件包。Example 工程也是独立可运行,且可在 Web 上开发调试来提高效率。

展示效果是为了增强读者的信心,完全是可以做出一套类似前端 vant 的跨端 UI 组件库,也是我们的目标,开源的话后续会考虑,当然你在读完本系列后,也可以很容易的构建出属于自己项目的组件库。

读者将会本文中得到:

  1. 避免多引擎组件开发的思维误区
  2. 从零开始搭建 Flutter 多引擎组件工程

思维误区

原本想把误区放在搭建过程中讲,但确实很重要,怕大家忽视了,所以提到最开始说。

注意 FlutterEngine 生命周期

在以前单引擎时代,使用 flutter_boost 等框架做 app_to_app 混合开发时,我们其实并不关注 FlutterEngine 的生命周期的,因为它会是一个单例,加载后不用释放。而在多引擎渲染中,实质上是通过 FlutterEngineGroup 创建出多个 FlutterEngine 实例,而这些实例从内存安全的角度考虑,是需要我们去管理生命周期的。

不要通过 Plugin 跟 Native 通信

在往常的 Flutter 开发中,大家已经十分习惯构建各种 Plugin 来进行 App 与 Flutter 的通信,大多数常用的三方库使用的也都是一样。

但在 Flutter 多引擎下,这实际上是不可行的。原因就是以前是1对1的链接关系,现在是1对多了(对应多个 FlutterEngine)。如果使用 Plugin 做多引擎渲染的通信实现,必须考虑如何去做 FlutterEngine 的区分和消息隔离,代码很容易变得臃肿和难以理解,这无疑大大增加了开发成本。

那我们要如何做呢?

官网推荐直接用 channel 或者用 pigeon 工具链。用 channel 需要我们做更多的事(Channel 绑定、模型映射),所以我们选择用 pigeon

有一点上篇文章没有讲到的,pigeon 生成后其实就是 messageChannel 通信,而且它的生命周期在我们的实现中相当于跟 FlutterEngine 绑定了,不仅做到内存安全,而且天然是相互隔离的。

在开发上肯定还会用到一些第三方库,避免不了出现使用 Plugin 的情况,可以看下这篇 《Flutter 多引擎渲染,组件支持 FlutterPlugin》,有单独发文的这个系列里不会细讲 ~

工程化

那现在我们需要搭建属于自己的组件工程了,先看一下我们的目录结构:

image.png

component_foundations 存放通用的工具类、基础 UI 等。

fgui 通用组件库

part_home 首页业务组件库

part_video 视频业务组件库

examples 示例工程

搭建入口

开始动手,先创建入口项目

flutter create components --template=package

入口项目的作用相当于 main,只负责聚合依赖各个组件库。

components.dart 实现也很简单,只是把组件库的入口继续 export

export 'package:part_home/ui_components.dart';
export 'package:fgui/ui_components.dart';
export 'package:part_video/ui_components.dart';

创建组件库

fgui 组件库为例:

flutter create fgui --ios-language=objc --android-language=java --template=plugin --platforms=ios,android

有仔细看上文的同学肯定会问,为什么不能使用 plugin 还要用 plugin 的方式创建呢?

这主要是省事,虽然我们不会使用 plugin,也不会修改生成的 plugin 文件,但我们会把 pigeon 生成的 iOS、Android 文件放入到相应的文件夹中。

生成完毕后,也需要创建上文 exportui_components.dart

image.png

这也是一个入口文件,用于聚合组件库内部的各个组件。

以非常常用的 Alert 组件为例:

import/export (略)

// 暴露 componentAlertDialog 给 Native
@pragma('vm:entry-point')
void componentAlertDialog() { 
  SwitchConfigMaker.runByMutiEngines = true; // 可先忽略,意思是表明当前调用方式是在多引擎
  return runApp(const fgui_alert_dialog.AlertDialog()); 
}

...

构建组件

// StatefulWidget ,组件的状态由外部控制
class AlertDialog extends StatefulWidget {
 
  const AlertDialog({
    Key? key,
    ...
  }) : super(
          key: key,
          ...
        );

  @override
  _AlertDialogState createState() {
    return _AlertDialogState();
  }
}

class _AlertDialogState extends State<AlertDialog> {
  @override
  Widget build(BuildContext context) {
    // 这里需注意用 Directionality 
    return Directionality(
      textDirection: TextDirection.ltr,
      child: ...,
    }
  }
  
  ...
}

Directionality 是笔者推荐使用的根结点,因为很多 Flutter 官方组件都是需要这个文字顺序的,以前开发为什么不需要,是因为 MaterialApp 都帮我们做掉了,但在 multiple_flutters 中,不允许根结点为 MaterialApp,这也是很合理的,但确实会给开发者带来很多麻烦。

后续会推荐使用更完善的封装定义(MeterialWidget),有兴趣的同学可以看下这篇《Flutter 多引擎渲染,TextField 踩坑指南》

image.png

本篇着重讲 Flutter 侧工程建设,所以具体代码暂时不提。

引入 pigeon

组件就绪,我们现在需要把通信层使用上。

增加依赖

在 fgui 的 pubspec.yaml 中增加

dev_dependencies:
  pigeon:

源文件入口

image.png

构建文件 .api/alert_dialog.dart

为什么是隐藏文件?因为我们并不需要手动输出这个文件,后续在跨端工具链一节中会讲到。这里先用手动创建的方式,明白它的工作原理。

import 'package:pigeon/pigeon.dart';

class AlertDialogConfig {
  /// 对话框标题
  String? title;

  /// 对话框内容
  String? content;

  /// 确认按钮文案
  String? confirmText;

  /// 是否显示取消按钮
  bool? showCancel;

  /// 取消按钮文案
  String? cancelText;
}

@HostApi()
abstract class AlertDialogHostAPI {
  /// 点击确定
  void onClickConfirm();

  /// 点击取消
  void onClickCancel();
}

@FlutterApi()
abstract class AlertDialogFlutterAPI {
  /// 初始化配置
  void config(AlertDialogConfig maker);

  /// 关闭弹窗
  void dismiss();

  /// 更新确定按钮是否可点击
  void updateConfirmEnable(bool enable);
}

AlertDialogConfig 是构造的初始化配置类

AlertDialogHostAPI 定义了组件的回调方法

AlertDialogFlutterAPI 定义了调用组件的方法

执行脚本

执行脚本,生成 Flutter、iOS、Android 的通信层

flutter pub run pigeon \
  --input .api/alert_dialog.dart \
  --dart_out lib/.caches/alert_dialog.api.dart \
  --objc_header_out ios/Classes/APIs/FGUIAlertDialogAPI.h \
  --objc_source_out ios/Classes/APIs/FGUIAlertDialogAPI.m \
  --objc_prefix FGUI \
  --java_out  android/src/main/java/com/gaoding/apis/FGUIAlertDialogAPI.java \
  --java_package "com.gaoding.flutter.components.api"

在执行脚本之前,还需要检查 lib/.cachesios/Classes/APIsandroid/src/main/java/com/gaoding/apis 目录是否已存在,不存在需要创建。

可以看出,这脚本十分的繁琐,且不会帮助我们自动创建相应的目录,而且生成上只能一次执行1个文件,这也坚定了我们后续把跨端工具链先做起来的决心。

成功生成后,我们就在 pigeon 的帮助下,把通信层的 API 建设出来了

生成代码解释

Flutter

lib/.caches/alert_dialog.api.dart

image.png

我们定义的点击确认回调,可以看到通信原理就是构建了一个 BasicMessageChannel 把消息发送给 Native 侧。

image.png

Native 调用 Flutter,过程是相反的。所以这里生成的是一个 setup() 用于监听 Native 发送的消息回调。如果不用 pigeon 上述的通信过程就要自己实现,十分繁琐。

iOS

直接看 .m 文件 FGUIAlertDialogAPI.m

image.png

图上就是 iOS 监听 onClickConfirm 事件回调,原理就是等待 BasicMessageChannel 从 Flutter 发送过来的消息,然后回调给 onClickConfirmWithError: 代理。

onClickConfirmWithError: 代理定义当然在 .h 文件中

image.png

iOS 调用 Flutter 方法:

image.png

直接调用相应的 API 即可。

Android

image.png

Android 上也是类似的,setup 提供 Android 监听事件回调的方式。

image.png

api.onClickConfirm()interface, 需要外部实现。

Android 调用 Flutter 方法,也是很简单,直接调用即可。

image.png

模型映射

除开通信 MessageChannel 的封装,pigeon 做的更多的一件事就是模型映射了。这点其实和调用后端 API 类似了,传输过程都还是 JSON Map 对象,各端提供序列化/反序列化方法实现各端模型一致性。

Flutter:

image.png

Android:

image.png

iOS:

image.png

可以看到,其实也都是硬转换的,没有用什么反射方法,也符合 Flutter 的设计。当然这些对自动生成的工具链来说很简单,甚至性能比反射还更好些。

但也能看出通过这种方式,是要避免传输重型对象的,比如 Bitmap 等等。就算是复杂的组件也应该拥有简化的 API ~

后续

总结一下,本文如何实现多引擎渲染组件 Flutter 工程化及入口建设,以及对 pigeon 生成文件的解析。即入口层、通信层这两个部分实现。

下一篇将会讲明我们是如何把多引擎渲染组件跟 pigeon 生成的文件结合起来的。即 Flutter Widget 基类的作用。

传送门

《从零开始|构建 Flutter 多引擎渲染组件:Flutter 代码篇》


感谢阅读,如果对你有用请点个赞 ❤️