引言
在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。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 通信方案,但在大规模落地场景下,该方案存在一系列不足:
- 手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。
-
大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。
-
代码封装繁琐:Channel API 较为底层,开发者还需二次封装,才能提供给业务方便调用,这一过程较为繁琐。
除 Channel 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。
基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。
Unify 介绍
Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。
Unify 的亮点特性包括:
- 平台无关的模块抽象: 允许开发者使用 Dart 语言声明与平台无关的模块接口与实体。
-
灵活的实现注入: 开发者可以灵活地选择注入原生实现(Android/iOS)或 Flutter 实现。
-
自动代码生成: 借助强大的代码生成引擎,Unify 可以自动生成 Flutter、Android、iOS 多平台下统一调用的 SDK。
下面是一个使用 Unify 声明原生模块的示例:
java
@UniNativeModule()
abstract class DeviceInfoService {
Future<DeviceInfoModel> getDeviceInfo();
}
通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:
bash
DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {
print("${deviceInfoModel.encode()}");
});
Unify 的整体架构如下:
Unify 核心概念
在进行混合通信开发时,典型场景包括:
- 将原生模块导出至 Flutter 调用
- 将 Flutter 模块导出至原生调用
- 在接口中传递复杂实体类
在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。
在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:
- 实现注入接口:用于开发者注入实现逻辑
-
对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。
-
对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。
- 三端统一调用接口
-
Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。
-
在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。
-
值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。
整体流程如下图所示:
具体来说:
|------------------|----------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
| 概念 | 描述 | 举例 |
| UniNativeModule | 声明一个模块,该模块的实现在原生(Android/iOS)注入。 通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。 | |
| UniFlutterModule | 声明一个模块,该模块的实现在 Flutter 注入。 通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。 | |
| UniModel | Unify 提供的模板注解之一,主要作用: * 创建自定义实体(Model/Entity)。 跨端传输时,可以把它的对象实体作为参数, 直接跨端发送。 | |
Getting Start
前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。
在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。
注:完整代码实现可于文末点击「阅读原文」查看。
Step1:模块声明
第一步,开发者需要对模块接口进行声明。在 Flutter 工程根目录下创建一个 interface 目录,所有 Unify 的模块声明均位于该目录中。
interface 下包含两个声明文件,均以 Dart 抽象类方式编写。
device_info_service.dart
声明原生模块
java
// device_info_service.dart
@UniNativeModule()
abstract class DeviceInfoService {
/// 获取设备信息
Future<DeviceInfoModel> getDeviceInfo();
}
@UniNativeModule 注解表示该模块的实现由原生侧提供。
device_info_model.dart
声明返回值 Model
javascript
// device_info_model.dart
@UniModel()
class DeviceInfoModel {
/// 系统版本
String? osVersion;
/// 内存信息
String? memory;
/// 手机型号
String? plaform;
}
@UniModel 注解表示这是一个跨平台的数据模型。
值得一提的是:
- Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。
-
在 Unify 中,实体类支持任意嵌套。
-
通过 Unify 生成器,interface 中声明的实体类(UniModel)将同时生成 Android(Java)、iOS(Objective-C)、Flutter(Dart)实现代码,在任意一端下,开发者都以同样方式使用实体类,由 Unify 实现底层序列化、反序列化及透传。
Step2:执行 Unify 生成器
接口声明完成后,执行如下命令生成跨平台代码:
properties
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,将会生成两部分代码:
- 实现注入接口:
-
Android:DeviceInfoService.java、DeviceInfoServiceRegister.java
-
iOS:DeviceInfoService.h、DeviceInfoService.m
- 三端统一调用接口:
-
Flutter:main.dart
-
Android:MainActivity.java
-
iOS:AppDelegate.m
注:代码文件源自 Unify/example/01_uninativemodule_demo
值得一提的是:
-
除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。
-
本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。
Step3:注入原生实现
有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:
Android 实现
java
public class DeviceInfoServiceImpl implements DeviceInfoService {
@Override
public void getDeviceInfo(Result<DeviceInfoModel> result) {
DeviceInfoModel model = new DeviceInfoModel();
......
result.success(model);
}
}
iOS 实现
objectivec
// 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 封装的原生模块了:
模块调用
bash
OutlinedButton(
child: const Text("获取设备信息"),
onPressed: () {
DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {
setState(() {
_platformVersion = "\n${deviceInfoModel.encode()}";
});
});
},
),
效果截图
至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!
小结
通过这个示例,我们体验了 Unify 带来的价值:
- 统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致
- UniModel: 支持跨平台透明传输的数据模型
- 相比 Flutter 原生 Channel 方式:
-
避免手动解析参数易出错
-
Android、iOS 双端自动对齐
-
大量 Channel 自动生成,易于维护
-
复杂实体无缝序列化,降低管理成本
我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:
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 的整体流程如下:
Unify 多语言代码生成器
基于这套 Unify AST,Unify 自研了一套多语言代码生成器,能够基于一套 AST 同时生成多端、多语言代码(Java、Dart、Objective-C),这也是 Unify 高效开发的关键。
在 Unify AST 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:
Unify AST
Unify AST 节点多语言映射
基于 Unify AST,以 UniModel 为例,开发者声明的 UniModel 将被转换为 Model AST 实例:
有了 Model AST,Unify 声明了 UniModel 在多端下的生成代码模板。在 Unify 中,我们自研了一套类似于 Flutter 组件化的代码生成模板语法,相较于其它框架手动拼接字符串的方式,Unify 代码生成模板结合 Unify AST 具备更高的模版编写效率,同时代码质量和可维护性更高。以 UniModel 为例,部分模版如图:
Unify 代码生成器的作用是将 UniModel 的 Model AST 与各技术栈下的生成模版相结合,从而生成 UniModel 在各平台下的多语言实现。最终的生成代码如图:
同类方案对比
Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。同时,Unify 也逐渐演化出自身特色,比如参数支持任意嵌套的实体类、集合类范型,以及贴近 Flutter 开发者的纯 Dart 语言的接口声明方式。
Unify 还支持批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。外卖大规模 Flutter 落地之初,面临数10+基础能力的批量导出,如果逐个搭建 Git 库导出,维护成本和导出成本过高。基于 Unify 的批量导出能力,我们在短时间内完成了对平台能力的批量封装。
基于前文的使用介绍、原理介绍,相信大家对 Unify 有了深入的了解。在本节中,我们将 Unify 与其它同类框架对比,帮助大家选型、决策。
通过对比可以看出,不同方案各有特色,适合于不同的场景。概括来说,如果业务中有大量封装导出场景,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 在业务中多端落地如图所示:
落地收益
滴滴国际化外卖业务包含用户端、骑手端、商家端三端,目前均已实现 Flutter 大规模业务落地,并且 Flutter 均已覆盖各端核心主流程,实现跨端复用,整体交付提效 50%+,收益显著,并且是一项持续性提效的收益。其中,国际化外卖骑手端 90%+ 以上代码均为 Flutter 跨端实现,已线上稳定运行两年多时间。
目前,Unify 已成为滴滴 DiFlutter 技术体系的核心架构组件之一,稳定支撑着各端业务,并在业务中大量使用,解决了基础模块、业务模块的混合通信问题,彻底解决了由 Channel 通信导致的参数手动解析错误、Android/iOS 双端接口抽象不一致等问题。
滴滴国际化外卖 Flutter 部分业务落地场景展示:
总结与未来展望
滴滴国际化外卖在完成大规模 Flutter 跨端落地之后,我们意识到 Flutter 跨端仍然存在进一步提效空间,目前在向纯 Flutter 化方向演进。对于未来 Unify 的演进,我们希望将 Unify 打造成一套 Flutter 混合开发领域的标准化解决方案,帮助业务解决 Flutter 大规模落地过程中的痛点难点问题。
目前,Unify 已经完成混合通信能力的沉淀,未来我们将持续迭代,提供更多功能,让跨端混合通信开发更加高效、可靠。今年上半年,我们也调研了 Flutter PlatformView 嵌原生能力,目前 Unify 正在提供一套基于嵌原生的混合路由方案,解决大规模 Flutter 落地场景下的混合页面跳转问题。
新的混合路由相较于业界已有方案,更加轻量化,大幅降低复杂度。我们希望这套路由能够助力业务,向纯 Flutter 化方向演进、过渡。经过多年验证稳定后,我们也荣幸得将 Unify 作为滴滴官方开源项目,将这套实践分享给业内同行。欢迎大家试用、体验、star 支持!
国际化外卖技术团队正在招聘服务端高级开发工程师、高级数据研发工程师,感兴趣的小伙伴欢迎联系ginasun@didiglobal.com,期待你的加入!
更多项目开源信息,欢迎点击「阅读原文」了解!