拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

鸿蒙Flutter适配:用dia实现依赖注入与架构解耦

鸿蒙Flutter适配:用dia实现依赖注入与架构解耦

最近团队开始把 Flutter 项目往鸿蒙上迁,第一周的状态基本是:一边装环境,一边清点三方库的适配状态。清点结果不出意外——大量纯 Dart 生态的包装库都能用,而凡是沾了原生平台插件的,全都在等鸿蒙侧的实现。我当时的想法是,先把依赖注入这个基础设施层搞定,因为架构干净不干净,很大程度取决于依赖怎么管理。于是选中了 dia。这个库在 Flutter 圈子里不算顶流,但它恰好是“纯 Dart + 无代码生成 + 类型安全”三个特性的交集,鸿蒙化适配成本极低,却能带来很实在的依赖注入与工程解耦收益。这篇文章就记录了我把 dia 弄到鸿蒙 Flutter 工程里的完整过程,包括环境、配置、代码、踩坑,以及一个可以直接照抄的架构模板。适合正在做鸿蒙 Flutter 适配,或者想在项目里引入依赖注入但不想碰 get_it 那套手动注册姿势的人。

1. dia 到底是干嘛的:一个“无感”的依赖注入框架

1.1 为什么是 dia,而不是 get_it、provider

Flutter 生态里做依赖注入,多数人第一个想到的是 get_it,再往后是 provider、riverpod 这类状态管理库顺手把依赖也管了。dia 的定位不太一样,它是专注于依赖注入本身的轻量框架。用最直白的话说,dia 解决的是“一个对象怎么创建、怎么找到它的依赖、怎么被其他人使用”的问题,而且整个过程尽量不打扰业务代码。

对比一下几个主流方案:

方案依赖注入方式是否要代码生成类型安全心智负担
get_it手动注册,Service Locator不需要弱,运行时强转低,但注册散落各处
injectable + get_it注解 + 代码生成需要 build_runner中等中,需要维护生成代码
providerInheritedWidget 包装不需要弱,上下文查找中,偏状态管理
riverpod全局 Provider + 编译期检查不需要强较高,API 多
dia模块化注入器 + 运行时注册表不需要强,按 Type 精确解析低,结构统一

当时我选 dia 有几个很实际的理由。第一,它的依赖解析是基于 Dart 的Type在运行时查注册表,不需要反射,也不需要代码生成,这直接避开了“鸿蒙 Flutter 分支是否支持 build_runner 产物”这类麻烦事。第二,它把一个 App 的依赖关系收敛到“模块”里,不散落在 main 函数或各个页面中,这种结构对工程解耦来说非常关键。第三,它本身的依赖树很干净,打开 pubspec 看,几乎没有什么传递依赖,意味着鸿蒙化时不需要处理一堆间接依赖的兼容问题。

1.2 核心概念速览:注入器、模块、绑定

用 dia 之前,先搞清楚三个概念:DiaInjector是注入器的核心入口,负责持有所有已注册的绑定;模块(Module)是我们把相关依赖放在一起的载体;绑定关系则描述“当我要求某个类型时,应该用哪个工厂方法创建什么对象”。

代码长这样,看着就很好理解:

import 'package:dia/dia.dart'; @module class CoreModule { @provide Dio dio() => Dio(BaseOptions(baseUrl: 'https://api.example.com')); @provide ApiClient apiClient(Dio dio) => ApiClient(dio); } void main() { final injector = DiaInjector(); injector.addModule(CoreModule()); final api = injector.get<ApiClient>(); }

注意到apiClient(Dio dio)这个方法了吗?dia 在创建ApiClient的时候,会先去解析Dio这个参数,然后自动把上一步创建的Dio实例传进来。这意味着业务代码里不需要new、不需要查找全局对象、不需要记忆创建顺序,只声明“我需要什么”,剩下的交给注入器。这就是“无感依赖注入”的含义——组件只声明依赖,不关心依赖从哪来、怎么销毁、是不是单例。

1.3 为什么纯 Dart 库最适合当鸿蒙适配的第一站

鸿蒙化适配时,三方库大致分三类。第一类是纯 Dart 库,只依赖 SDK 和基础包,这类适配基本就是“改版本号、跑通编译”的事。第二类是有 MethodChannel 的插件,需要鸿蒙侧提供原生实现,这类最典型的就是各种定位、支付、推送 SDK,只能等官方或社区适配。第三类是既依赖平台能力又依赖 C++ 引擎的,比如地图渲染、视频播放,适配工作量最大。

dia 属于第一类。它的核心逻辑跟平台无关,不碰dart:io、不碰 MethodChannel、不依赖任何原生 view。所以把它鸿蒙化,真正的难点不在 dia 本身,而在于鸿蒙 Flutter 工程的环境搭建和构建配置。这反而给了我们一个很好的切入点:先把最干净的一类库打通,积累鸿蒙工程的构建经验,后面再适配复杂库时,至少不会在基础配置上浪费时间。

2. 鸿蒙化适配的环境搭建与前置检查

2.1 鸿蒙 Flutter 开发环境怎么配

这里先把大前提说清楚:HarmonyOS NEXT 上跑 Flutter,用的不是官方原版 Flutter SDK,而是鸿蒙生态维护的定制分支。安装时要注意,不要用flutter doctor检查完就以为万事大吉,鸿蒙 Flutter 分支需要单独准备。

我实际操作时的环境清单:

  • 鸿蒙版 Flutter SDK(通过 gitee 或厂商镜像获取,区分 stable 和 dev 分支)
  • DevEco Studio(鸿蒙原生 IDE,用来编译 HAP 包和调试原生侧代码)
  • 鸿蒙 SDK 与 Platform Tools(跟随 DevEco Studio 安装即可)
  • 一台鸿蒙手机或模拟器(建议先拿模拟器跑通,真机再处理签名)

环境变量方面,建议把鸿蒙版 Flutter 的 bin 目录单独指出来,不要跟官方 Flutter 混用。我一开始图省事,直接把两个 SDK 配了同一个 PATH,结果flutter --version时灵时不灵,非常坑。后来把官方 Flutter 的路径从 PATH 里摘掉,只在需要的时候手动切换。

2.2 适配前的“体检”:分析 dia 的依赖边界

拿到鸿蒙 Flutter 环境后,别急着把 dia 往工程里怼。先做一次“依赖体检”,这一步能帮你省下后面大量的排查时间。

体检第一步,看 dia 的 pubspec.yaml 里依赖了什么。以 dia 当前版本为例,它的依赖极其精简,基本只有 meta 和几个纯 Dart 辅助库,不涉及fluttersdk 以外的平台相关包。体检第二步,翻源码。打开 dia 的 lib 目录,搜索dart:io、dart:ffi、MethodChannel等关键字符串,如果搜索结果为零,说明这个库在鸿蒙上不会因为平台能力缺失而报错。

我之所以强调这一步,是因为“纯 Dart 包”和“能在鸿蒙编译的纯 Dart 包”之间还隔着一条线:有些纯 Dart 包装库会隐式依赖dart:io的文件读写、网络等能力。比如某些日志库、本地存储封装,虽然声明是纯 Dart,但用了dart:io的文件句柄,到了鸿蒙侧如果底层能力没对齐,编译能过但运行时会崩。dia 在这块很干净,体检直接通过。

2.3 工程结构:Flutter 工程与鸿蒙工程的联动关系

鸿蒙 Flutter 工程的结构跟安卓 Flutter 工程很像,但也有明显差异。用鸿蒙版 Flutter 创建项目后,你会看到ohos目录,而不是android目录。整个联动关系是:

  • lib/目录:Dart 业务代码,跨端共享,鸿蒙和安卓/ iOS 用同一套
  • ohos/目录:鸿蒙原生工程结构,里面包含entry模块、oh-package.json5、build-profile.json5、module.json5等
  • Flutter 产物:Dart 代码最终由鸿蒙 Flutter 引擎加载,原生侧通过一个继承自FlutterAbility的入口类来承载 Flutter 页面

在这个结构下,dia 这种纯 Dart 库的适配范围基本被限制在lib/和pubspec.yaml层面。也就是说,只要 pub 依赖能拉下来,Dart 代码能编译,dia 原则上就能跑。原生侧的oh-package.json5不需要做任何改动,这点比适配平台插件要省心太多。

3. 动手适配:让 dia 在鸿蒙工程里跑起来

3.1 pubspec.yaml 与版本约束处理

进入实操阶段。首先在鸿蒙 Flutter 工程的 pubspec.yaml 里加上 dia:

dependencies: flutter: sdk: flutter dia: ^4.0.0

然后执行flutter pub get。这一步通常会暴露第一个坑:鸿蒙版 Flutter 内置的 Dart SDK 版本可能落后于 dia 的 SDK 约束。如果报错内容是The current Dart SDK version is X, but dia requires SDK version ^Y,那就要做版本匹配。

我的处理办法是:先查当前鸿蒙 Flutter SDK 的 Dart 版本,然后去 pub.dev 查 dia 的历史版本,找一个 Dart SDK 约束兼容的版本。不要盲目升级 dia 到最新版,鸿蒙分支的 Dart 版本往往追不上官方最新,强行用最新版只会让 pub get 卡死。必要时还可以用dependency_overrides强制指定某个版本的 meta 包,但注意这属于非常规操作,只有 meta 这样的纯接口包才适合这么做。

3.2 编译期适配的 3 个关键改动

跑通 pub get 之后,接着是编译。我第一次编译时踩到三个问题,逐一记录。

第一个是 lints 版本冲突。dia 依赖的分析插件推荐了比较新的 lints 规则,而鸿蒙 Flutter 工程模板自带的analysis_options.yaml还是老版本,导致flutter analyze一堆警告。严格说这不影响编译,但会影响后续 CI。解决方法很简单,把analysis_options.yaml里的 include 路径调整成鸿蒙 SDK 推荐的版本,或者直接注释掉冲突项。

第二个是产物输出路径。鸿蒙工程的构建产物不是安卓的 aar,而是 HAP(鸿蒙应用包),构建入口从gradlew assembleRelease变成了hvigorw assembleHap。如果你之前习惯了安卓那一套,可能会在ohos目录下找 aar 找不到。记住:flutter build负责产 Flutter 资产,hvigorw负责把整个 HAP 打出来,两者是先后关系。

第三个是 Dart 编译目标。鸿蒙 Flutter 分支对 Dart 的 AOT 和 JIT 支持路径跟官方版有差异,偶尔会在编译时出现Unsupported option之类的提示。我的建议是,flutter run调试时用 debug 模式,发布包用flutter build hap --release,不要手动给 hvigor 传太多额外参数,默认流程最稳。

3.3 最小验证:一个 Hello 级别的注入用例

工程能编译之后,先做一个最小验证,确认 dia 在鸿蒙运行时环境里真的能工作。这一步不要搞复杂的业务代码,就写一个最简单的注入链。

class GreetingService { String greet(String name) => 'Hello, $name'; } @module class AppModule { @provide GreetingService greetingService() => GreetingService(); } void main() { WidgetsFlutterBinding.ensureInitialized(); final injector = DiaInjector(); injector.addModule(AppModule()); final service = injector.get<GreetingService>(); debugPrint(service.greet('HarmonyOS')); runApp(const MyApp()); }

在真机或模拟器上跑起来,看到日志输出Hello, HarmonyOS,说明 dia 的注入器在鸿蒙 Flutter 引擎里工作正常。这里有个细节值得强调:WidgetsFlutterBinding.ensureInitialized()一定要在DiaInjector初始化之前调用。因为在鸿蒙 Flutter 分支上,部分引擎能力是懒加载的,如果注入器初始化时触发了插件注册,而 binding 还没初始化,可能会白屏或直接闪退。

最小验证通过后,适配工作其实已经完成了大半。剩下的问题从“能不能跑”变成了“怎么用得更好”,这恰恰是工程解耦的核心。

4. 实战:用 dia 给鸿蒙 Flutter 项目做一次架构解耦

4.1 场景设计:从网络层到业务层的依赖梳理

我拿一个实际业务场景来演示:登录模块。假设我们有一个AuthApi负责网络请求,一个AuthRepository负责把网络数据转成业务模型并做本地缓存,一个LoginViewModel负责给登录页提供状态和事件。如果不做依赖注入,这三个类的创建关系会散落在页面初始化、全局单例、甚至 static 方法里。做了解耦之后,依赖关系应该一目了然:

class AuthApi { AuthApi(this._dio); final Dio _dio; Future<UserProfile> login(String username, String password) async { // 网络请求逻辑 } } class AuthRepository { AuthRepository(this._api, this._cache); final AuthApi _api; final LocalCache _cache; Future<UserProfile> login(String username, String password) async { final profile = await _api.login(username, password); await _cache.save('user_profile', profile); return profile; } } class LoginViewModel extends ChangeNotifier { LoginViewModel(this._repository); final AuthRepository _repository; // 页面状态和事件 }

关键点在于:LoginViewModel不知道AuthRepository怎么创建,AuthRepository不知道AuthApi和LocalCache怎么创建。每个类只负责自己那一层的事,构造参数就是依赖声明,完全不依赖任何全局变量。这为后面的鸿蒙适配省了大麻烦——因为鸿蒙侧如果要替换网络库或缓存库,只需要改模块配置,不需要动业务代码。

4.2 模块拆分与绑定配置

有了上面的类设计,接下来用 dia 把它们绑起来。我推荐按“层”来拆模块,这样职责更清晰。

@module class NetworkModule { @provide(singleton: true) Dio dio() => Dio(BaseOptions(baseUrl: 'https://api.example.com')); @provide(singleton: true) LocalCache localCache() => LocalCache(); } @module class DataModule { @provide AuthApi authApi(Dio dio) => AuthApi(dio); @provide(singleton: true) AuthRepository authRepository(AuthApi api, LocalCache cache) => AuthRepository(api, cache); } @module class PresentationModule { @provide LoginViewModel loginViewModel(AuthRepository repo) => LoginViewModel(repo); }

初始化代码收敛到一处:

final injector = DiaInjector() ..addModule(NetworkModule()) ..addModule(DataModule()) ..addModule(PresentationModule());

在页面侧使用时,可以直接通过注入器取:

class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { late final LoginViewModel _viewModel; @override void initState() { super.initState(); _viewModel = DiaInjector.get<LoginViewModel>(); } // 页面其他逻辑 }

这种方式的好处是,页面不自己 new ViewModel,测试时也可以在外部换成带 mock 依赖的 ViewModel 再传给页面。

4.3 解耦的收益如何验证

很多团队看了解耦的文章觉得有道理,但不知道收益怎么衡量。我给两个可以量化的验证角度。

第一个是“替换实现需要改几个文件”。比如我们要把网络层从真实请求换成 mock 数据来做 UI 联调。在未解耦的代码里,你需要找到所有Dio()的 new 表达式、所有AuthApi的构造点,逐个替换,通常要改十几个文件。有了 dia 之后,只需要再写一个 mock 模块:

@module class MockDataModule { @provide AuthApi authApi() => MockAuthApi(); }

然后初始化时把DataModule换成MockDataModule,其他所有业务代码一行都不用动。

第二个是“单元测试里能不能随手造出被测对象”。以LoginViewModel为例,如果它的依赖来自构造参数,而不是从全局函数里获取,那么测试时直接创建一个带 mock repository 的实例就行:

final mockRepo = MockAuthRepository(); final viewModel = LoginViewModel(mockRepo);

如果MockAuthRepository实现了AuthRepository的接口,测试就完全不需要碰网络和缓存。这在鸿蒙化的过程中尤其重要:因为早期鸿蒙设备环境不稳定,依赖注入带来的可测试性,能让你在没拿到真机前就完成大部分逻辑验证。

5. 常见问题与排查技巧实录

5.1 编译失败高频报错速查表

现象原因解决
pub get 报 Dart SDK 版本不满足鸿蒙 Flutter SDK 的 Dart 版本低于 dia 要求降低 dia 版本,或用 dependency_overrides 指定兼容版本的 meta 包
编译报Unsupported option给 hvigor 传了官方 Flutter 的参数使用默认构建命令,不要手动添加 Gradle 风格参数
flutter build hap找不到产物构建输出目录跟安卓不同在ohos/entry/build下找 HAP,或直接看 DevEco Studio 的构建日志
启动后白屏WidgetsFlutterBinding未先初始化在DiaInjector初始化前调用ensureInitialized
注入时报类型找不到模块未注册或模块类没被引用检查addModule是否调用,以及模块文件是否被 import 进 main 依赖链

这里想单独说一句关于“模块类没被引用”的坑。Dart 的 tree shaking 在 release 模式下会把“看起来没被引用”的代码删掉。如果你在 main 函数里只调用了injector.addModule(NetworkModule()),而NetworkModule类没有被其他任何代码显式引用,理论上它是会被保留的,因为 addModule 的实参就是一个实例。但如果你把模块类当成纯注解类,期望通过反射去发现,那在鸿蒙 Flutter 的 release 产物里大概率会被剪掉。dia 的设计决定了你必须手动 addModule,这恰恰是它的优势——少了一层不可控的代码发现机制。

5.2 运行期 Injector 解析异常的排查思路

运行期最常遇到的是ProviderNotFoundException,也就是注入器里找不到某个类型的绑定。我的排查思路分三步。

第一步,确认模块注册顺序。如果A依赖B,而B的模块没有在A之前注册,解析A时就会失败。dia 允许你通过参数直接声明依赖,所以模块之间最好按照“基础能力 -> 数据层 -> 页面层”的顺序注册。

第二步,检查是不是命名了不同类型的绑定。Dart 的Type是区分泛型参数的,ApiClient<HttpApi>和ApiClient<UserApi>是两个不同的 key。如果你在模块里绑定了ApiClient<UserApi>,去解析ApiClient<HttpApi>自然找不到。

第三步,排查循环依赖。比如A构造时需要B,B构造时需要A,这在运行时表现为栈溢出或超时。解决办法是:把其中一个依赖改为懒加载,在工厂方法里改成() => injector.get<A>()而不是直接接收参数。这种写法我见过很多团队踩坑,尤其是在接缓存和配置类时特别容易出现。

5.3 几个容易忽略的工程坑

除了编译和运行报错,还有几个工程层面的坑值得记录。

坑一:不要把 dia 的模块类全部写在一个文件里。模块是架构边界,天然应该按业务拆。全写在一个文件里虽然编译没问题,但团队协作时合并冲突会非常多。我的习惯是每个业务模块一个xxx_module.dart,核心模块单独放core_module.dart。

坑二:注意单例的生命周期。dia 的singleton: true绑定在注入器内部是全局的,如果你在鸿蒙侧开了多个 Ability 或多次启动 Flutter 引擎,注入器对象如果是每个引擎单独创建的,单例是各引擎内独立;如果注入器是全局静态的,单例会跨页面共享。建议按“引擎生命周期”来管理注入器,不要图省事直接 static 全局。

坑三:release 包的混淆问题。鸿蒙 Flutter 在打正式包时可能开启代码压缩或混淆,如果 dia 是按Type解析,有些混淆器会改动类名,导致运行时期望的类型和注册的类型对不上。处理办法很简单:在鸿蒙原生侧或 Flutter 构建配置里,对使用了 dia 的 dart 库关闭裁剪,或者把核心类的混淆规则加入白名单。这个坑在安卓早期也出现过,鸿蒙生态的工具链还不算太成熟,提前留个心眼能少被坑一次。

6. 最后再分享一点我的体会

这次把 dia 鸿蒙化的过程,让我对“跨端适配”这件事有了新的认识。很多人觉得鸿蒙适配难在技术,其实难在“不确定性工具链”和“三方依赖边界不清”的组合。像 dia 这种依赖边界特别干净的库,反而是最适合拿来探路的——它逼你把工程结构、构建流程、模块划分都理顺,又不是特别耗费时间。你在实际操作中也可以用我的清单去检查其它纯 Dart 库:依赖树干净吗?用了 dart:io 吗?依赖的 Dart SDK 版本跟鸿蒙分支匹配吗?这三关过了,基本就能在鸿蒙上跑。

最后一个小技巧:适配完 dia 后,建议马上把项目的初始化逻辑按照“模块化”整理一遍,不要边迁边留一堆全局单例。等鸿蒙生态越来越成熟,后面再接入其他三方库时,你会发现架构的底气,全都在这一步打下来了。

返回列表