开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第11天,点击查看活动详情
前文传送门
《从零开始|构建 Flutter 多引擎渲染组件:先导篇》 《从零开始|构建 Flutter 多引擎渲染组件:Flutter 工程篇》
前文讲述了 Flutter 多引擎渲染组件的入口层及通信层建设,本篇将讲述 Flutter 组件代码如何与 pigeon 的生成产物结合起来,构造多端通用的 UI 组件库。
读者将会本文中得到:
- 如何将 pigeon Flutter 产物结合到项目中。
- 如何优雅的构建 Base Widget 来抹平开发感知。
- (伪)一个完整警告框(Alert)示例。
本篇将以前文的 alert_dialog 为示例,具体讲解我们是怎么样一步步落地的。
结合 pigeon 产物
前文我们已经用 pigeon 生成了 alert_dialog.api.dart,我们在开发使用上,关键在于去结合生成的2个类 AlertDialogFlutterAPI 和 AlertDialogHostAPI。
AlertDialogHostAPI
HostAPI 比较好理解,调用 Native 方法,就是 UI 组件的回调。
import './alert_dialog.api.dart';
class AlertDialogState extends State<AlertDialog> {
late AlertDialogHostAPI hostAPI;
// MARK: LifeCircle
@override
void initState() {
super.initState();
hostAPI = AlertDialogHostAPI(); // 初始化实例
...
}
/// 封装点击确定的方法
void onClickConfirm() {
if (widget.onClickConfirm != null) {
// 如果 Flutter 有实现,说明是 Flutter 调用的组件,则直接回调
// 当然,你也可以用上文中定义的 runByMutiEngines 来判断
widget.onClickConfirm!();
return;
}
// 调用 pigeon hostAPI
hostAPI.onClickConfirm().catchError((error) {
debugPrint("hostAPI onClickConfirm no exists");
});
}
...
}
前文遗漏了一点说明,我们的跨端 UI 组件库也是可以在 Flutter 代码中直接调用的。比如前文的 example 工程示例 gif,就是在 web 上调试开发,毕竟在 add_to_app 混合开发中,attach 的方式实在是不尽人意。
组件开发上就可以使用封装的 onClickConfirm() 来忽略回调源头是 Flutter 还是 iOS/Android。
AlertDialogFlutterAPI
FlutterAPI 比较麻烦一点,它是 Native 调用 Flutter 组件方法的 API,需要 Flutter 侧 setup 的方式去监听。
首先我们先实现 AlertDialogFlutterAPI 抽象类的方法,提供 CallBack 回调更好用。
// 实现 AlertDialogFlutterAPI 方法
class AlertDialogFlutterAPIHandle extends AlertDialogFlutterAPI {
// 提供 config 初始化方法回调
final void Function(AlertDialogConfigMaker maker) makerCallback;
// 提供 dismiss 方法回调
final void Function() dismissCallback;
AlertDialogFlutterAPIHandle({
required this.makerCallback,
required this.dismissCallback,
});
// 实现 config()
@override
void config(AlertDialogConfig maker) {
// 这里暂时忽略,后文会讲
var item = AlertDialogConfigMaker(
title: maker.title!,
content: maker.content!,
confirmText: maker.confirmText!,
showCancel: maker.showCancel!,
cancelText: maker.cancelText!,
);
makerCallback(item);
}
// 实现 dismiss()
@override
void dismiss() {
return dismissCallback();
}
}
提供实现后,在使用上就比较简单了
late AlertDialogConfigMaker maker;
...
@override
void initState() {
super.initState();
hostAPI = AlertDialogHostAPI();
// 如果是 Flutter 调用,则由 Flutter 注入初始化参数,否则实现一个默认的 maker,减少布局困难
maker = widget.maker ?? AlertDialogConfigMaker();
...
// 这里写过前端的同学就很容易明白,内外 this 指向不一致,所以声明一个 that
var that = this;
// 调用 AlertDialogFlutterAPI setup 方法
AlertDialogFlutterAPI.setup(AlertDialogFlutterAPIHandle(
makerCallback: (maker) {
// 更新 maker
that.maker = maker;
...
// 重新布局,先 mark,后面再讲
setupUI();
},
dismissCallback: () {
that.dismiss();
},
));
}
/// 关闭弹窗
void dismiss() {
...
}
AlertDialogConfigMaker
仔细的同学会发现为什么我们还要重新构建一个 AlertDialogConfigMaker 对象?不是已经有 AlertDialogConfig 了吗?
原因有二:
一是AlertDialogConfig 是 pigeon 自动生成的,它里面是没有默认值的,我们希望 maker 的声明是自带默认值,对组件开发者无需各种判空,对外部使用者来说可以可选赋值使用。而使用继承或者重写的方式处理自动生成的代码太耦合,不入就重新声明一个。
二是我们希望对外不暴露 pigeon 的实现,它在组件内逻辑自闭,外部不应该操作它,所以我们放到了隐藏目录下 .caches/alert_dialog.api.dart。
class AlertDialogConfigMaker {
/// 是否是在多引擎中运行
static bool runByMutiEngines = false;
AlertDialogConfigMaker({
this.title = "",
this.content = "",
this.confirmText = "",
this.showCancel,
this.cancelText = "",
});
/// 对话框标题
String? title;
/// 对话框内容
String? content;
/// 确认按钮文案
String? confirmText;
/// 是否显示取消
bool? showCancel;
/// 取消按钮文案
String? cancelText;
}
到这里,一个粗糙的组件代码雏形已经完成了 ~
构建 Base Widget
先说说上文的代码粗糙在哪里?我们开发的是 UI 组件,但上面的那些代码并不是 UI 部分所要关心的,这样写在一起,会让整体代码变得十分臃肿,难于后续维护。
达成上述共识后,我们有几种选择来处理这种情况:
- 构建
AlertDialogAPIController用“组合”的方式来解耦。这在代码开发上应该是最推荐的,但这个场景上并不适合,一是我们的 API 跟生命周期也是强相关,这样避免不了产生“组合”的胶水代码。二是灵活性太高了,解决不了开发 A 组件的时候去引入了 BAPIController 导致的一系列问题。 - 用
with AlertDialogAPIMixin“混入”的方式,这在 Flutter 也是常用用法,但这里并没有选择它,笔者认为 Mixin 一定要是提供独立的通用能力,不能滥用不能滥用不能滥用(说三遍)。 - 用
extends AlertDialogBase“继承”的方式,比上面两种方式,更有代码局限性,限制了上限,但也限制了下限。对团队来讲,代码灵活性很高也不是一件好事,提供规范的能力在团队开发中更为重要。
所以,我们构建 AlertDialogBase 以及 AlertDialogStateBase 两个 Base 基类来隐藏 pigeon 实现,放在 .caches/alert_dialog.base.dart 中(这个为什么也放入隐藏目录,在“跨端工具链篇”细讲)。
// Alert 基类,且提供的是抽象类,一定要被实现。
abstract class AlertDialogBase extends StatefulWidget {
// 提供 Flutter 代码调用的方式
const AlertDialogBase({
Key? key,
this.maker,
this.onClickConfirm,
this.onClickCancel,
}) : super(key: key);
final AlertDialogConfigMaker? maker;
/// 点击确定
final void Function()? onClickConfirm;
/// 点击取消
final void Function()? onClickCancel;
}
// AlertDialogConfigMaker 放在 base.dart 中
class AlertDialogConfigMaker {
...
}
// AlertDialogFlutterAPIHandle 也放在 base.dart 中
class AlertDialogFlutterAPIHandle extends AlertDialogFlutterAPI {
...
}
// state 基类,也是抽象类
abstract class AlertDialogStateBase extends State<AlertDialogBase> {
// 在提供便捷的 hostAPI 给继承类直接使用
late AlertDialogHostAPI hostAPI;
late AlertDialogConfigMaker maker;
// MARK: LifeCircle
@override
void initState() {
...
}
// 抽象类的另一个好处,声明 protected,让实现类必须实现,减少疏漏
@protected
void dismiss();
那在组件实现者上,就只要关心具体 UI 布局即可。
import '../.caches/alert_dialog.base.dart';
export '../.caches/alert_dialog.base.dart';
/// Alert 对话框
///
/// AUTO LAYOUT
class AlertDialog extends AlertDialogBase {
final Function()? onClosed;
const AlertDialog({
Key? key,
AlertDialogConfigMaker? maker,
Function()? onClickCancel,
Function()? onClickConfirm,
this.onClosed,
}) : super(
key: key,
maker: maker,
onClickCancel: onClickCancel,
onClickConfirm: onClickConfirm,
);
@override
_AlertDialogState createState() {
return _AlertDialogState();
}
}
class _AlertDialogState extends AlertDialogStateBase {
// 这是一个新构造的生命周期,先mark,后面会讲
@override
void setupUI() {
...
}
// 对话框消失方法实现
@override
void dismiss() {
...
}
@override
Widget build(BuildContext context) {
return Directionality(
...
_textButton(
text: '确认',
onTap: () => onClickConfirm(), // 点击按钮调用回调
),
...
}
}
}
实现细节
setupUI 生命周期
有一个问题,Flutter 调用组件代码,是通过构造 AlertDialog 直接调用,initState() 中 maker = widget.maker。而在 iOS、Android 通过 FlutterEngineGroup 渲染组件时,maker 初始化完成是在 AlertDialogFlutterAPI.setup 回调中,这个是通过 Channel,时机上更晚。
那我们在组件开发上往往需要一个表明 maker 已经正确赋值后的时机。所以我们提供一个 setupUI() 时机,表明可以开始正确渲染了。具体代码也是在 alert_dialog.base.dart 中
abstract class AlertDialogStateBase extends State<AlertDialogBase> {
// MARK: LifeCircle
@override
void initState() {
super.initState();
...
maker = widget.maker ?? AlertDialogConfigMaker();
...
if (!AlertDialogConfigMaker.runByMutiEngines) {
setupUI(); // 非多引擎调用,则直接调用
return;
}
...
var that = this;
AlertDialogFlutterAPI.setup(AlertDialogFlutterAPIHandle(
makerCallback: (maker) {
that.maker = maker;
that._updateCurrentLocale(maker.currentLocale);
// :autolayout
setupUI(); // 多引擎调用赋值后调用
},
...
));
}
/// initMaker 后,启动 UI 布局时机
void setupUI() {}
Flutter 代码调用组件中的方法
Flutter 代码和 Native 代码是有矛盾点的,Flutter 是通过状态管理变更组件状态,而 iOS/Android 更多是通过函数式方法调用变更组件状态的(当然 Native 也有状态管理方案,但在跨端场景下也没用,所以忽略)。
比如 dismiss() 这个方法,Native 上是通过 AlertDialogFlutterAPI.setup() 监听调用的。而 Flutter 代码想要调用到 dissmiss() 有三种方案:
暴露状态
class AlertDialog extends AlertDialogBase {
final bool? visible; // 是否可见
}
这样符合 Flutter 的开发习惯,但却需要组件开发者处理更多的逻辑,dismiss() 方法和 widget.visible 变量需要绑定处理。
Key.currentState
var _key = GlobalKey<DialogLoadingState>();
AlertDialog(
key: _key,
...
)
...
_key.currentState?.dismiss();
这个方式的问题是需要暴露 _AlertDialogState 不能设置成私有,对于 StatefulWidget 来说,这也是破坏性的。
提供 Controller(伪)
这是 Flutter 官方提倡的,比如常用的 ScrollController,但不用的原因是代码处理代价还是比较高的。
但我们用了相同的思路,用 event_bus 实现效果。
在 alert_dialog.base.dart 处理。
...
import 'package:event_bus/event_bus.dart';
export 'package:event_bus/event_bus.dart';
/// 定义事件: 关闭弹窗
class DismissEvent {
DismissEvent();
}
/// 可选对话框-基类
abstract class AlertDialogBase extends StatefulWidget {
const AlertDialogBase({
Key? key,
this.eventBus,
...
}) : super(key: key);
// 定义一个外部可实现的 eventBus 对象
final EventBus? eventBus;
...
}
abstract class AlertDialogStateBase extends State<AlertDialogBase> {
...
// MARK: LifeCircle
@override
void initState() {
super.initState();
...
// 增加 eventBus 事件监听
widget.eventBus?.on<DismissEvent>().listen((event) {
// 调用 dismiss 方法
dismiss();
});
...
}
...
}
使用上
var _eventBus = EventBus();
AlertDialog(
// 注入 EventBus
eventBus: _eventBus,
...
}
...
_eventBus.fire(DismissEvent()); // 合适时机触发事件
这么使用虽然保证了内部处理的一致性,但用法上不够优雅,没有 Flutter 那味
这三种方式都有明显的优缺点,这里并没有确定一个最佳开发实践,读者们可以根据自己项目的实际情况斟酌使用。
后续
本文介绍了 Flutter 多引擎组件在 Flutter 侧开发要点,构建 Widget 基类,让 UI 组件开发更专注于布局样式上。没有讲具体布局,因为布局大同小异,放代码过多又容易被判定是水文 ~
下一篇会介绍在 iOS / Android 上,我们要做些什么来与 pigeon 产物结合,让 Native 调用更优雅。
预告
《从零开始|构建 Flutter 多引擎渲染组件:Native 调用篇》
《从零开始|构建 Flutter 多引擎渲染组件:跨端工具链篇》
感谢阅读,如果对你有用请点个赞 ❤️