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

947 阅读8分钟

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

前文传送门

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

前文讲述了 Flutter 多引擎渲染组件的入口层及通信层建设,本篇将讲述 Flutter 组件代码如何与 pigeon 的生成产物结合起来,构造多端通用的 UI 组件库。

读者将会本文中得到:

  1. 如何将 pigeon Flutter 产物结合到项目中。
  2. 如何优雅的构建 Base Widget 来抹平开发感知。
  3. (伪)一个完整警告框(Alert)示例。

本篇将以前文的 alert_dialog 为示例,具体讲解我们是怎么样一步步落地的。

结合 pigeon 产物

前文我们已经用 pigeon 生成了 alert_dialog.api.dart,我们在开发使用上,关键在于去结合生成的2个类 AlertDialogFlutterAPIAlertDialogHostAPI

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 部分所要关心的,这样写在一起,会让整体代码变得十分臃肿,难于后续维护。

达成上述共识后,我们有几种选择来处理这种情况:

  1. 构建 AlertDialogAPIController 用“组合”的方式来解耦。这在代码开发上应该是最推荐的,但这个场景上并不适合,一是我们的 API 跟生命周期也是强相关,这样避免不了产生“组合”的胶水代码。二是灵活性太高了,解决不了开发 A 组件的时候去引入了 BAPIController 导致的一系列问题。
  2. with AlertDialogAPIMixin “混入”的方式,这在 Flutter 也是常用用法,但这里并没有选择它,笔者认为 Mixin 一定要是提供独立的通用能力,不能滥用不能滥用不能滥用(说三遍)。
  3. extends AlertDialogBase “继承”的方式,比上面两种方式,更有代码局限性,限制了上限,但也限制了下限。对团队来讲,代码灵活性很高也不是一件好事,提供规范的能力在团队开发中更为重要

所以,我们构建 AlertDialogBase 以及 AlertDialogStateBase 两个 Base 基类来隐藏 pigeon 实现,放在 .caches/alert_dialog.base.dart 中(这个为什么也放入隐藏目录,在“跨端工具链篇”细讲)。

image.png

// 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 布局即可。

image.png

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 多引擎渲染组件:跨端工具链篇》


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