在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。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 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。
基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。
Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。
Unify 的亮点特性包括:
下面是一个使用 Unify 声明原生模块的示例:
@UniNativeModule()abstract class DeviceInfoService { Future<DeviceInfoModel> getDeviceInfo();}
通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:
DeviceInfoService.getDeviceInfo().then((deviceInfoModel) { print("${deviceInfoModel.encode()}");});
Unify 的整体架构如下:
在进行混合通信开发时,典型场景包括:
在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。
在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:
整体流程如下图所示:
具体来说:
概念 | 描述 | 举例 |
UniNativeModule | 声明一个模块,该模块的实现在原生(Android/iOS)注入。 通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。 | |
UniFlutterModule | 声明一个模块,该模块的实现在 Flutter 注入。 通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。 | |
UniModel | Unify 提供的模板注解之一,主要作用:
跨端传输时,可以把它的对象实体作为参数, 直接跨端发送。 |
前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。
在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。
注:完整代码实现可于文末点击「阅读原文」查看。
第一步,开发者需要对模块接口进行声明。在 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 注解表示这是一个跨平台的数据模型。
值得一提的是:
接口声明完成后,执行如下命令生成跨平台代码:
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,将会生成两部分代码:
注:代码文件源自
Unify/example/01_uninativemodule_demo
值得一提的是:
有了实现注入接口,开发者根据接口分别补充 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 DeviceInfoServiceVendorUNI_EXPORT(DeviceInfoServiceVendor)......#pragma mark - DeviceInfoService协议 实现- (void)getDeviceInfo:(void(^)(DeviceInfoModel* result))success fail:(void(^)(FlutterError* error))fail { DeviceInfoModel *model = [DeviceInfoModel new]; ...... success(model);}@end
对于完整代码,可参见文末「阅读原文」:
注:代码文件源自 Unify/example/01_uninativemodule_demo
一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:
模块调用
OutlinedButton( child: const Text("获取设备信息"), onPressed: () { DeviceInfoService.getDeviceInfo().then((deviceInfoModel) { setState(() { _platformVersion = "\n${deviceInfoModel.encode()}"; }); }); },),
效果截图
至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!
通过这个示例,我们体验了 Unify 带来的价值:
我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:
Unify 之所以能提升跨端通信的开发效率,关键在于 Unify 实现了一套多语言代码生成器,通过该生成器,能够自动解析开发者声明的 Dart 抽象接口,并自动生成三端注入、调用代码,将开发者从繁重的胶水代码中解脱出来。在本节中,介绍 Unify 底层代码生成原理,并介绍与同类方案的对比。
我们选择 Dart 语言作为模块接口声明语言,并基于 Dart Analyzer 库,实现对接口声明的静态分析,将 Dart 源代码转换为 Dart AST。在 Unify 中,我们基于 Dart AST 定义了 Unify AST,这是一套适用于模块导出场景的简化 AST,特色为内置了对多语言(Java、Dart、Objective-C)代码生成的映射关系,保证了后续多语言代码生成器实现的简洁。
从开发者接口声明,通过 Dart Analyzer 库静态分析,到产出 Unify AST 的整体流程如下:
基于这套 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 则是较好的选择。
在滴滴国际化外卖业务 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 支持!
作者:刘瑞刚
来源-微信公众号:滴滴技术
出处
:https://mp.weixin.qq.com/s/Di8czdY3KCqDAYrzEvePrg