本文已参加「新人创造礼」活动,一同敞开创造之路。

接受上文:《Flutter 多引擎烘托,在稿定 App 的实践》

在这么偏远的技术路线上,仍是有蛮多读者和社区认可的。所以笔者会把这计划的原理详细介绍给咱们,让咱们能少走一些弯路 ~

前语

先讲下关于 Flutter 开发架构的理解,大概存在这3种:

  1. Flutter 为主开发的 APP。
  2. Flutter 与 Native 容器混合型,页面可所以 Flutter,也可所以 Native,代表比方 flutter_boost。
  3. FlutterEngineGroup 多引擎烘托,容器是 Native 提供,Flutter 只关心 View 部分即可。

这里不是比较各自的优劣,选型上只选择最适合的办法。

像笔者公司前期是用 flutter_boost 做页面容器混合型,但现在架构上的变化,会逐渐削减 Native 的完成,变为跨端架构,而纯 Flutter 并不满足于咱们的开发,且从代码量上也不可能改为 Flutter 为主的 APP 架构。(dart 说实话也不是一个好的开发言语[手动狗头][勿喷])。

基于这个前提能选择的很少,Flutter 多引擎是完成跨端 UI 现在是最现实的计划而已。究竟官方也是只需 Demo,甚至官方引荐的 pigeon Demo 也没和 multiple_flutters Demo 联系起来。

至于为什么不持续运用容器混合型开发?咱们有没有感觉到 add_to_app 的办法开发调试起来也是蛮痛苦的,单元测验也不好做。而且要坚持事务层不动的情况下,开发许多额外的 plugins 来支撑 UI,这个本钱仍是很高的。

完成及原理

Flutter 多引擎渲染,在稿定 App 的实践(二):原理篇

整套计划完成下即为跨端 UI 组件化,如上图所示。

跨端 UI 组件化优势:

  1. APP 双端 UI 一致性完成,而且能够布置为独立的 Web Demo,提前进行 UI 走查。
  2. Flutter UI 组件独立开发调试,且只关心 API 界说,不关心详细完成。
  3. 处理开发运用痛点,削减开发难度曲线,主动生成调用 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 调用。

Flutter 多引擎渲染,在稿定 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 的完成都统一同来。

Flutter 多引擎渲染,在稿定 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 拆包的,所以问题也不大。

示例作用

讲了半响干货,没有放实际示例作用给咱们看下

Flutter 多引擎渲染,在稿定 App 的实践(二):原理篇

能够看到笔者开发调试都是在 Web 上,开发起来简略、轻松、明了。

由于也生成了 VO(ViewModel)代码,所以也天然的 VO / BO 代码分离。

也弥补下线上真实作用

Flutter 多引擎渲染,在稿定 App 的实践(二):原理篇

后续

这里边细节倒是有许多,篇(jing)幅(li)有限,先就这样,各位同学有什么疑问能够谈论上探讨 ~

下一部分:Flutter 多引擎烘托,在稿定 App 的实践(三):躺坑篇