开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情
前文我们讲了基础环境配置,以及需要提前了解的 Flutter 官方 Demo,但官方 Demo 的工程结构并不可取。
如何更优雅的建设属于 Flutter 多引擎渲染组件的工程结构是十分重要的事,我们希望 Flutter 多引擎渲染组件是十分独立的,可以跟原生 UI 无痕组合,也可以跟任何架构方案不产生冲突,比如 flutter_boost。当然我们也是这样实现的,先展示下我们已经实现了哪一些:
(视频转成 gif,帧数不够所以看起来有些卡顿)
上图中, fgui
(FGUI 是内部代号)是任意项目都能使用的通用型组件包,part_home
表明是首页的业务组件包,part_video
是视频编辑器的业务组件包。Example 工程也是独立可运行,且可在 Web 上开发调试来提高效率。
展示效果是为了增强读者的信心,完全是可以做出一套类似前端 vant
的跨端 UI 组件库,也是我们的目标,开源的话后续会考虑,当然你在读完本系列后,也可以很容易的构建出属于自己项目的组件库。
读者将会本文中得到:
- 避免多引擎组件开发的思维误区
- 从零开始搭建 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》,有单独发文的这个系列里不会细讲 ~
工程化
那现在我们需要搭建属于自己的组件工程了,先看一下我们的目录结构:
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 文件放入到相应的文件夹中。
生成完毕后,也需要创建上文 export
的 ui_components.dart
这也是一个入口文件,用于聚合组件库内部的各个组件。
以非常常用的 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 踩坑指南》
本篇着重讲 Flutter 侧工程建设,所以具体代码暂时不提。
引入 pigeon
组件就绪,我们现在需要把通信层使用上。
增加依赖
在 fgui 的 pubspec.yaml
中增加
dev_dependencies:
pigeon:
源文件入口
构建文件 .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/.caches
,ios/Classes/APIs
,android/src/main/java/com/gaoding/apis
目录是否已存在,不存在需要创建。
可以看出,这脚本十分的繁琐,且不会帮助我们自动创建相应的目录,而且生成上只能一次执行1个文件,这也坚定了我们后续把跨端工具链先做起来的决心。
成功生成后,我们就在 pigeon
的帮助下,把通信层的 API 建设出来了
生成代码解释
Flutter
lib/.caches/alert_dialog.api.dart
我们定义的点击确认回调,可以看到通信原理就是构建了一个 BasicMessageChannel
把消息发送给 Native 侧。
Native 调用 Flutter,过程是相反的。所以这里生成的是一个 setup()
用于监听 Native 发送的消息回调。如果不用 pigeon
上述的通信过程就要自己实现,十分繁琐。
iOS
直接看 .m 文件 FGUIAlertDialogAPI.m
图上就是 iOS 监听 onClickConfirm
事件回调,原理就是等待 BasicMessageChannel
从 Flutter 发送过来的消息,然后回调给 onClickConfirmWithError:
代理。
onClickConfirmWithError:
代理定义当然在 .h 文件中
iOS 调用 Flutter 方法:
直接调用相应的 API 即可。
Android
Android 上也是类似的,setup
提供 Android 监听事件回调的方式。
api.onClickConfirm()
是 interface
, 需要外部实现。
Android 调用 Flutter 方法,也是很简单,直接调用即可。
模型映射
除开通信 MessageChannel
的封装,pigeon 做的更多的一件事就是模型映射了。这点其实和调用后端 API 类似了,传输过程都还是 JSON Map 对象,各端提供序列化/反序列化方法实现各端模型一致性。
Flutter:
Android:
iOS:
可以看到,其实也都是硬转换的,没有用什么反射方法,也符合 Flutter 的设计。当然这些对自动生成的工具链来说很简单,甚至性能比反射还更好些。
但也能看出通过这种方式,是要避免传输重型对象的,比如 Bitmap 等等。就算是复杂的组件也应该拥有简化的 API ~
后续
总结一下,本文如何实现多引擎渲染组件 Flutter 工程化及入口建设,以及对 pigeon 生成文件的解析。即入口层、通信层这两个部分实现。
下一篇将会讲明我们是如何把多引擎渲染组件跟 pigeon 生成的文件结合起来的。即 Flutter Widget 基类的作用。
传送门
《从零开始|构建 Flutter 多引擎渲染组件:Flutter 代码篇》
感谢阅读,如果对你有用请点个赞 ❤️