滴滴开源新项目Unify:聚焦Flutter与原生通信难题,助力跨端应用落地

引言

在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。Flutter 作为近年来热门的跨端技术,以高性能、自渲染、泛跨端著称,得到广泛应用。在滴滴国际化业务中,我们大量应用 Flutter。目前已在滴滴国际化外卖、滴滴国际化出行司机端等业务中大规模落地,整体交付提效 50%+,收益显著。在大规模 Flutter 跨端场景下,存量的原生业务与增量 Flutter 业务间的双向通信成为痛点问题。

为此,滴滴国际化外卖自研 Unify 框架,旨在解决大规模跨端落地场景下,Flutter 与原生模块之间的通信问题。Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。

基于 Unify,滴滴国际化外卖成功将 16+ 个原生平台能力,26+ 个原生业务能力高效导入 Flutter,并沉淀出 UniFoundation、UniBusiness 两套业务架构模式,有效支撑外卖业务从零到一实现 Flutter 跨端落地。同时,Unify 也在滴滴国际化出行司机端中推广落地,有效支撑了兄弟业务的大规模跨端落地。

目前,Unify 已作为滴滴开源项目,正式开源,欢迎大家试用、体验、star 支持!

背景

在跨端落地过程中,通常会保留原生实现,以迭代方式逐步试水跨端,先跑通模式,再逐渐扩大跨端落地规模。

在原生代码与 Flutter 代码并存前提下,面临一系列实际问题:

1. 大量原生 SDK 如何高效导入 Flutter?

2. 大量业务功能如何高效导入 Flutter?

3. Flutter 功能模块如何导出给原生?

此类 Flutter 与原生代码间的双向通信问题,我们统称为混合通信问题

针对这一问题,Flutter 官方提供了 Channel 通信方案,但在大规模落地场景下,该方案存在一系列不足:

  1. 手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。

  1. 大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。

  2. 代码封装繁琐:Channel API 较为底层,开发者还需二次封装,才能提供给业务方便调用,这一过程较为繁琐。

除 Channel 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。

基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。

Unify 介绍

Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。

Unify 的亮点特性包括:

  • 平台无关的模块抽象: 允许开发者使用 Dart 语言声明与平台无关的模块接口与实体。

  • 灵活的实现注入: 开发者可以灵活地选择注入原生实现(Android/iOS)或 Flutter 实现。

  • 自动代码生成: 借助强大的代码生成引擎,Unify 可以自动生成 Flutter、Android、iOS 多平台下统一调用的 SDK。

下面是一个使用 Unify 声明原生模块的示例:

@UniNativeModule()
abstract class DeviceInfoService {
  Future<DeviceInfoModel> getDeviceInfo();
}

通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:

DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {
  print("${deviceInfoModel.encode()}");
});

Unify 的整体架构如下:

outside_default.png

Unify 核心概念

在进行混合通信开发时,典型场景包括

  • 将原生模块导出至 Flutter 调用

  • 将 Flutter 模块导出至原生调用

  • 在接口中传递复杂实体类

在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。

在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:

  1. 实现注入接口:用于开发者注入实现逻辑

  1. 对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。

  2. 对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。

  1. 三端统一调用接口

  1. Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。

  2. 在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。

  3. 值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。

整体流程如下图所示:

outside_default.png

具体来说:

概念描述举例

UniNativeModule

声明一个模块,该模块的实现在原生(Android/iOS)注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniFlutterModule

声明一个模块,该模块的实现在 Flutter 注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

outside_default.png

UniModel

Unify 提供的模板注解之一,主要作用:

  • 创建自定义实体(Model/Entity)。

跨端传输时,可以把它的对象实体作为参数,

直接跨端发送。

outside_default.png


Getting Start

前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。

在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。

注:完整代码实现可于文末点击「阅读原文」查看。

Step1:模块声明

第一步,开发者需要对模块接口进行声明。在 Flutter 工程根目录下创建一个 interface 目录,所有 Unify 的模块声明均位于该目录中。

interface 下包含两个声明文件,均以 Dart 抽象类方式编写。

device_info_service.dart

声明原生模块

// device_info_service.dart
@UniNativeModule()
abstract class DeviceInfoService {
  /// 获取设备信息
  Future<DeviceInfoModel> getDeviceInfo();
}

@UniNativeModule 注解表示该模块的实现由原生侧提供。

device_info_model.dart

声明返回值 Model

// device_info_model.dart
@UniModel()
class DeviceInfoModel {
  /// 系统版本
  String? osVersion;


  /// 内存信息
  String? memory;


  /// 手机型号
  String? plaform;
}

@UniModel 注解表示这是一个跨平台的数据模型。

值得一提的是:

  1. Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。

  1. 在 Unify 中,实体类支持任意嵌套。

  2. 通过 Unify 生成器,interface  中声明的实体类(UniModel)将同时生成 Android(Java)、iOS(Objective-C)、Flutter(Dart)实现代码,在任意一端下,开发者都以同样方式使用实体类,由 Unify 实现底层序列化、反序列化及透传。

Step2:执行 Unify 生成器

接口声明完成后,执行如下命令生成跨平台代码:

flutter pub run unify api\
  --input=`pwd`/interface \
  --dart_out=`pwd`/lib \
  --java_out=`pwd`/android/src/main/java/com/example/uninativemodule_demo \
  --java_package=com.example.uninativemodule_demo \
  --oc_out=`pwd`/ios/Classes \
  --dart_null_safety=true \
  --uniapi_prefix=UD

在命令中,指定了 interface 接口目录,Android、iOS 输出位置等配置信息。

在 2.1 节中说到,对于 UniNativeModule,将会生成两部分代码:

  1. 实现注入接口:

  1. Android:DeviceInfoService.java、DeviceInfoServiceRegister.java

  2. iOS:DeviceInfoService.h、DeviceInfoService.m

  1. 三端统一调用接口:

  1. Flutter:main.dart

  2. Android:MainActivity.java

  3. iOS:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

值得一提的是:

  1. 除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。

  2. 本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。

Step3:注入原生实现

有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:

Android 实现

public class DeviceInfoServiceImpl implements DeviceInfoService {
    @Override
    public void getDeviceInfo(Result<DeviceInfoModel> result) {
        DeviceInfoModel model = new DeviceInfoModel();
        ......
        result.success(model);
    }
}

iOS 实现

// DeviceInfoServiceVendor.h
@interface DeviceInfoServiceVendor : NSObject<DeviceInfoService>
@end


// DeviceInfoServiceVendor.m
@implementation DeviceInfoServiceVendor
UNI_EXPORT(DeviceInfoServiceVendor)
......
#pragma mark - DeviceInfoService协议 实现
- (void)getDeviceInfo:(void(^)(DeviceInfoModel* result))success fail:(void(^)(FlutterError* error))fail {
    DeviceInfoModel *model = [DeviceInfoModel new];
    ......
    success(model);
}
@end

对于完整代码,可参见文末「阅读原文」:

  • Android 平台实现:DeviceInfoServiceImpl.java

  • Android 平台注册实现:MainActivity.java

  • iOS 平台实现类:DeviceInfoServiceVendor.h、DeviceInfoServiceVendor.m

  • iOS 平台注册实现:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo

Step4:在 Flutter 中调用

一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:

模块调用

OutlinedButton(
  child: const Text("获取设备信息"),
  onPressed: () {
    DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {
      setState(() {
        _platformVersion = "\n${deviceInfoModel.encode()}";
      });
    });
  },
),

效果截图

outside_default.png

至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!

小结

通过这个示例,我们体验了 Unify 带来的价值:

  1. 统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致

  1. UniModel: 支持跨平台透明传输的数据模型

  1. 相比 Flutter 原生 Channel 方式:

  1. 避免手动解析参数易出错

  2. Android、iOS 双端自动对齐

  3. 大量 Channel 自动生成,易于维护

  4. 复杂实体无缝序列化,降低管理成本

我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:

outside_default.png

Unify 核心原理

Unify 之所以能提升跨端通信的开发效率,关键在于 Unify 实现了一套多语言代码生成器,通过该生成器,能够自动解析开发者声明的 Dart 抽象接口,并自动生成三端注入、调用代码,将开发者从繁重的胶水代码中解脱出来。在本节中,介绍 Unify 底层代码生成原理,并介绍与同类方案的对比。

Dart 代码静态分析

我们选择 Dart 语言作为模块接口声明语言,并基于 Dart Analyzer 库,实现对接口声明的静态分析,将 Dart 源代码转换为 Dart AST。在 Unify 中,我们基于 Dart AST 定义了 Unify AST,这是一套适用于模块导出场景的简化 AST,特色为内置了对多语言(Java、Dart、Objective-C)代码生成的映射关系,保证了后续多语言代码生成器实现的简洁。

从开发者接口声明,通过 Dart Analyzer 库静态分析,到产出 Unify AST 的整体流程如下:

outside_default.png

Unify 多语言代码生成器

基于这套 Unify AST,Unify 自研了一套多语言代码生成器,能够基于一套 AST 同时生成多端、多语言代码(Java、Dart、Objective-C),这也是 Unify 高效开发的关键。

在 Unify AST 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:

Unify AST

outside_default.png

Unify AST 节点多语言映射

outside_default.png

基于 Unify AST,以 UniModel 为例,开发者声明的 UniModel 将被转换为 Model AST 实例:

outside_default.png

有了 Model AST,Unify 声明了 UniModel 在多端下的生成代码模板。在 Unify 中,我们自研了一套类似于 Flutter 组件化的代码生成模板语法,相较于其它框架手动拼接字符串的方式,Unify 代码生成模板结合 Unify AST 具备更高的模版编写效率,同时代码质量和可维护性更高。以 UniModel 为例,部分模版如图:

outside_default.png

Unify 代码生成器的作用是将 UniModel 的 Model AST 与各技术栈下的生成模版相结合,从而生成 UniModel 在各平台下的多语言实现。最终的生成代码如图:

outside_default.png

同类方案对比

Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。同时,Unify 也逐渐演化出自身特色,比如参数支持任意嵌套的实体类、集合类范型,以及贴近 Flutter 开发者的纯 Dart 语言的接口声明方式。

Unify 还支持批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。外卖大规模 Flutter 落地之初,面临数10+基础能力的批量导出,如果逐个搭建 Git 库导出,维护成本和导出成本过高。基于 Unify 的批量导出能力,我们在短时间内完成了对平台能力的批量封装。

基于前文的使用介绍、原理介绍,相信大家对 Unify 有了深入的了解。在本节中,我们将 Unify 与其它同类框架对比,帮助大家选型、决策。

outside_default.png

通过对比可以看出,不同方案各有特色,适合于不同的场景。概括来说,如果业务中有大量封装导出场景,Unify 能够实现更高的批量导出效率,同时保持了较低的工程复杂度,易于维护。如果是对单模块进行封装导出,或者需要支持更多语言,尤其是 C++ 封装支持,Pigeon 则是较好的选择。

Unify 业务最佳实践

在滴滴国际化外卖业务 Flutter 大规模落地的初期,面临十余个公司平台能力 SDK 需要导出的 Flutter 侧,同时业务中存在大量混合通信,需要保证高可靠性。基于这一背景,在调研已有方案后,我们自研了 Unify,解决了大量模块的批量导出问题。并且在此过程中,我们沉淀出两套架构模式 UniFoundation 和 UniBusiness,成为业务混合通信最佳实践。

UniFoundation 是我们基于 Unify,高效完成公司16+ 个 SDK 批量导出,形成一套能够在 Android、iOS、Flutter 三端统一调用的基建能力。UniFoundation 是一套可复用基建,支撑了国际化外卖商家端、用户端、骑手端三端 Flutter 大规模落地。同时,作为通用基建,UniFoundation 成功推广到国际化出行司机端,助力兄弟业务的 Flutter 大规模落地,并实现跨团队合作共建。

在 UniFoundation 落地之后,在各端业务中,也存在大量业务模块与 Flutter 之间混合通信的场景,于是我们沿用 UniFoundation 的模式延伸出 UniBusiness。UniBusiness 是业务端内部,基于 Unify 批量抽象出的平台无关的业务模块,能够在三端,以统一的方式实现模块调用、复杂实体透传。随着 Flutter 落地规模的扩大,有越来越多业务模块由 Flutter 实现,并经过 Unify 封装,实现三端统一调用。

UniFoundation 和 UniBusiness 在业务中多端落地如图所示:

outside_default.png

落地收益

滴滴国际化外卖业务包含用户端、骑手端、商家端三端,目前均已实现 Flutter 大规模业务落地,并且 Flutter 均已覆盖各端核心主流程,实现跨端复用,整体交付提效 50%+,收益显著,并且是一项持续性提效的收益。其中,国际化外卖骑手端 90%+ 以上代码均为 Flutter 跨端实现,已线上稳定运行两年多时间。

目前,Unify 已成为滴滴 DiFlutter 技术体系的核心架构组件之一,稳定支撑着各端业务,并在业务中大量使用,解决了基础模块、业务模块的混合通信问题,彻底解决了由 Channel 通信导致的参数手动解析错误、Android/iOS 双端接口抽象不一致等问题。

滴滴国际化外卖 Flutter 部分业务落地场景展示:

outside_default.png

outside_default.png

总结与未来展望

滴滴国际化外卖在完成大规模 Flutter 跨端落地之后,我们意识到 Flutter 跨端仍然存在进一步提效空间,目前在向纯 Flutter 化方向演进。对于未来 Unify 的演进,我们希望将 Unify 打造成一套 Flutter 混合开发领域的标准化解决方案,帮助业务解决 Flutter 大规模落地过程中的痛点难点问题。

目前,Unify 已经完成混合通信能力的沉淀,未来我们将持续迭代,提供更多功能,让跨端混合通信开发更加高效、可靠。今年上半年,我们也调研了 Flutter PlatformView 嵌原生能力,目前 Unify 正在提供一套基于嵌原生的混合路由方案,解决大规模 Flutter 落地场景下的混合页面跳转问题。

新的混合路由相较于业界已有方案,更加轻量化,大幅降低复杂度。我们希望这套路由能够助力业务,向纯 Flutter 化方向演进、过渡。经过多年验证稳定后,我们也荣幸得将 Unify 作为滴滴官方开源项目,将这套实践分享给业内同行。欢迎大家试用、体验、star 支持!

国际化外卖技术团队正在招聘服务端高级开发工程师、高级数据研发工程师,感兴趣的小伙伴欢迎联系ginasun@didiglobal.com,期待你的加入!


更多项目开源信息,欢迎点击「阅读原文」了解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值