本文已参加「新人创造礼」活动,一同敞开创造之路。
接受上文:《Flutter 多引擎烘托,在稿定 App 的实践》
在这么偏远的技术路线上,仍是有蛮多读者和社区认可的。所以笔者会把这计划的原理详细介绍给咱们,让咱们能少走一些弯路 ~
前语
先讲下关于 Flutter 开发架构的理解,大概存在这3种:
- Flutter 为主开发的 APP。
- Flutter 与 Native 容器混合型,页面可所以 Flutter,也可所以 Native,代表比方 flutter_boost。
- FlutterEngineGroup 多引擎烘托,容器是 Native 提供,Flutter 只关心 View 部分即可。
这里不是比较各自的优劣,选型上只选择最适合的办法。
像笔者公司前期是用 flutter_boost 做页面容器混合型,但现在架构上的变化,会逐渐削减 Native 的完成,变为跨端架构,而纯 Flutter 并不满足于咱们的开发,且从代码量上也不可能改为 Flutter 为主的 APP 架构。(dart 说实话也不是一个好的开发言语[手动狗头][勿喷])。
基于这个前提能选择的很少,Flutter 多引擎是完成跨端 UI 现在是最现实的计划而已。究竟官方也是只需 Demo,甚至官方引荐的 pigeon Demo 也没和 multiple_flutters Demo 联系起来。
至于为什么不持续运用容器混合型开发?咱们有没有感觉到 add_to_app 的办法开发调试起来也是蛮痛苦的,单元测验也不好做。而且要坚持事务层不动的情况下,开发许多额外的 plugins 来支撑 UI,这个本钱仍是很高的。
完成及原理
整套计划完成下即为跨端 UI 组件化,如上图所示。
跨端 UI 组件化优势:
- APP 双端 UI 一致性完成,而且能够布置为独立的 Web Demo,提前进行 UI 走查。
- Flutter UI 组件独立开发调试,且只关心 API 界说,不关心详细完成。
- 处理开发运用痛点,削减开发难度曲线,主动生成调用 ComponentAPI 给 Native 侧无感调用抹平开发运用本钱。
下面会从开发流程的视点,逐渐分析整套计划的完成要害点。
界说 UI 组件
组件界说采用 YAML 标准化言语界说
RULE 界说
界说 | 阐明 |
---|---|
name | 组件称号 |
init | 初始化数据 → List<{ name(称号)、type(类型)、note(注释)、default(默许值) }> |
options | 额外装备→{ note(组件注释)、autolayout(是否是主动布局) } |
properties | 组件属性→ List<{ name(称号)、type(类型)、note(注释)、default(默许值) }> |
methods | 提供对外办法→ List<{ name(称号)、note(注释)、inputs(入参 List) }> |
cb_methods | 提供回调办法→ 同上 |
classes | 界说 Class→ List< name(称号)、note(注释)、properties(属性 List)> |
TYPE 支持
Flutter(界说) | iOS | Android |
---|---|---|
String | NSString | String |
int/long/double | int/long/double | Int/Long/Double |
bool | BOOL | Boolean |
Map | NSDictionary | Map |
List | NSArray | List |
List<Class> | NSArray<id<ClassProtocol>> | List<Class> |
Image | UIImage | Bitmap |
后续有需求会持续弥补:比方 Color、Class extends Class、Class use Class
示例
ui_components.yaml
以最开始开发的 Switch 组件为例(后续上都以它为例),界说如下:
- name: Switch
init:
- { name: title, type: String, note: 标题, default: -- }
- { name: textColor, type: String, note: 默许(封闭)文字色彩, default: "255,255,255,0.4" }
- { name: textColorAtOn, type: String, note: 在敞开时文字色彩, default: "255,255,255,1" }
options:
note: GUI 切换按钮组件
autolayout: false
properties:
- { name: "on", type: bool, note: 是否敞开, default: false }
FGUIComponentAPI 生成 Flutter 开发套件
生成的调用类分为多个部分,.gitigore 即为主动生成的文件
文件结构如下:
FlutterProject/ # Flutter 项目目录
↓ fgui/ # Flutter GUI Kit 组件库
↓ ui_components.yaml # 界说组件
↓ ui_components.dart # 调用进口层(.gitigore)
↓ .api/ # API 索引,主动生成,被用于 pigeon(.gitigore)
↓ lib/ # lib
↓ .caches/ # 组件基类及 API 实例,主动生成(.gitigore)
↓ switch.api.dart # pigeon 生成类
↓ switch.base.dart # 组件基类,用于封装 api.dart
↓ {switch}/{switch.dart} # **进行组件开发**
进口层(ui_components.dart)
@pragma('vm:entry-point')
void componentSwitch() => runApp(const fgui_switch.Switch());
多引擎的进口有必要是 root 节点的办法,且有必要完成 runApp。
API 索引(.api/)
// AUTO GENERATE API
//
// NOTES: 主动生成,无需手动修改.
import 'package:pigeon/pigeon.dart';
class SwitchConfig {
/// 标题
String? title;
/// 默许(封闭)文字色彩
String? textColor;
/// 在敞开时文字色彩
String? textColorAtOn;
/// 「通用」当时环境言语<lang:, country:>
Map? currentLocale;
/// 「通用」屏幕宽度(pt)
double? screenWidth;
/// 「通用」屏幕高度(pt)
double? screenHeight;
}
@HostApi()
abstract class SwitchHostAPI {
/// 触发埋点
void windTrack(String eventName, int eventID, Map detailInfo);
/// 布局视图巨细
void contentSize(double width, double height);
/// 更新-是否敞开
void fUpdateOn(bool on);
}
@FlutterApi()
abstract class SwitchFlutterAPI {
/// 初始化装备
void config(SwitchConfig maker);
/// 是否敞开
void on(bool? on);
/// 「通用」更新埋点弥补信息
void updateWindSupplementaryInfo(Map<String?, Object?> windSupplementaryInfo);
}
以上代码是依据组件 YAML 界说,经过 FGUIComponentAPI 生成的,主要作用是提供给 pigeon 组件进行 xx.api.dart 代码生成。
开发基类(xx.base.dart)
pigeon 的作用仅仅多端的 messageChannel 封装,离咱们想要的组件基类其实有很大的间隔,这个咱们能够去体会下就知道了。
所以调用基类的作用是进一步封装 pigeon 的 api.dart,让开发者无感知是一个对 App 的组件,只需调用/完成 base.dart 的办法,就能够做到独立调用以及给 add_to_app 调用。
如上图所示,
基类对 on
属性的 set / get 重写,在设置上,如果是独立运用,那会走 widget.fUpdateOn(on) 办法,如果是 add_to_app 的办法,那就会调用 api.dart 中的 host.fUpdateOn(on) 通知给 Native,Native 就会经过 messageChannerl 收到音讯。
@protected 的办法便是组件开发需求完成的办法,比方这边 Native 需求跨端组件的宽度进行布局。
开发侧(xx.dart)
/// GUI 切换按钮组件
///
/// FIXED LAYOUT
class Switch extends SwitchBase {
const Switch({Key? key, EventBus? eventBus}) : super(key: key, eventBus: eventBus);
@override
_SwitchState createState() {
return _SwitchState();
}
}
class _SwitchState extends SwitchStateBase {
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(), // Replace it!
);
}
@override
void updateCurrentLocale(Locale locale) {
setState(() {});
}
}
以上也是 FGUIComponentAPI 生成的初始代码,也是为了避免一些坑。
比方最外层用 Directionality 包裹,是由于 multiple_flutters 不能是以 MaterialApp 作为根,而如果疏忽了 Directionality,那在 add_to_app 有些完成会报错,比方 ListView,由于它需求文字排序办法,这个许多人都会疏忽掉,由于 main.dart 都根本是以 MaterialApp 作为根的,它内置了 Directionality 完成。
还有一点比较有趣的规划,由于 Flutter 规划上是状况驱动,而不是办法驱动,所以生成上也加入了最简略的 EventBus 办法,让独立运行以及 add_to_app 的完成都统一同来。
比方在测验 Demo 中,经过 UpdateBannersEvent 来直接修改组件数据,跟 App 调用 updateBanners 办法坚持一致。
当然,测验工程也是主动生成的,只需填补要害代码即可。
FGUIComponentAPI 生成双端调用类
iOS 端
从 官方示例 咱们能够得知:
一个 FlutterEngineGroup 包含多个 FlutterEngine 实例
FlutterEngine 实例创立上需求指定 Entrypoint,这个便是咱们上面进口层声明的 componentSwitch
每个 FlutterEngine 有必要是 FlutterViewController 来承载
那咱们需求对外封装成一个 View 来让 iOS 调用层运用。
//
// FGUISwitch.h
// AUTO GENERATE API
//
// NOTES: 主动生成,无需手动修改.
//
#import "FGUIComponent.h"
NS_ASSUME_NONNULL_BEGIN
@interface FGUISwitchInitConfig : NSObject
/// 标题
@property(nonatomic, nullable, copy) NSString *title;
/// 默许(封闭)文字色彩
@property(nonatomic, nullable, copy) NSString *textColor;
/// 在敞开时文字色彩
@property(nonatomic, nullable, copy) NSString *textColorAtOn;
@end
/// [Flutter]: GUI 切换按钮组件
@interface FGUISwitch : FGUIComponent
- (instancetype)initWithMaker:(void(^)(FGUISwitchInitConfig *make))block hostVC:(UIViewController *)hostVC contentSizeDidUpdateBlock:(void(^)(CGSize contentSize))contentSizeDidUpdateBlock;
// MARK: - ContentSize
- (CGSize)intrinsicContentSize;
// MARK: - Properties
/// 是否敞开
@property (nonatomic, assign) BOOL on;
@property (nonatomic, copy) void(^fUpdateOnBlock)(BOOL on);
// MARK: - Customer Blocks
// MARK: - Public Methods
// MARK: - Creators
@end
NS_ASSUME_NONNULL_END
以上便是示例主动生成的调用 h 文件
m 文件过长,这里疏忽展示,里边为了削减依靠以及多项目运用,是经过反射的方式生成调用代码。
要害点是需求外部传入一个 hostVC,内部经过 addChild 的方式将 FlutterViewController 加入到 hostVC 上。
Android 端
按官方示例是代码布局的方式,但按照 Android 小伙伴们的习气,咱们改成了支持 xml 布局的方式。
/**
* AUTO GENERATE API
* NOTES: 主动生成,无需手动修改.
*/
...
/**
* GUI 切换按钮组件
*/
class FGUISwitch : FrameLayout {
private var mFragmentManager: FragmentManager? = null
private var mEngineBinding: FGUISwitchBinding? = null
private val entryPoint = "componentSwitch"
private var _on: Boolean = false
/**
* 初始化
* @param title 标题
* @param textColor 默许(封闭)文字色彩
* @param textColorAtOn 在敞开时文字色彩
*/
fun init(
fragmentManager: FragmentManager,
title: String = "--",
textColor: String = "255,255,255,0.4",
textColorAtOn: String = "255,255,255,1",
) {
...
}
/**
* 设置 是否敞开
*/
fun setOn(on: Boolean) {
_on = on
mEngineBinding?.on(on)
}
/**
* 是否敞开
*/
fun getOn(): Boolean {
return _on
}
...
}
这里也简略的把生成的调用部分放出来供咱们参考。
特别说一下,由于 Android 不能用 Interface 的方式模拟 Class(这点 OC 真的是太好反射了)所以只能是直接依靠的 Flutter 的包,不过优点是,Android 里 Flutter 的包是依据 FlutterPlugin 拆包的,所以问题也不大。
示例作用
讲了半响干货,没有放实际示例作用给咱们看下
能够看到笔者开发调试都是在 Web 上,开发起来简略、轻松、明了。
由于也生成了 VO(ViewModel)代码,所以也天然的 VO / BO 代码分离。
也弥补下线上真实作用
后续
这里边细节倒是有许多,篇(jing)幅(li)有限,先就这样,各位同学有什么疑问能够谈论上探讨 ~
下一部分:Flutter 多引擎烘托,在稿定 App 的实践(三):躺坑篇