鸿蒙Flutter适配:用dia实现依赖注入与架构解耦 最近团队开始把 Flutter 项目往鸿蒙上迁第一周的状态基本是一边装环境一边清点三方库的适配状态。清点结果不出意外——大量纯 Dart 生态的包装库都能用而凡是沾了原生平台插件的全都在等鸿蒙侧的实现。我当时的想法是先把依赖注入这个基础设施层搞定因为架构干净不干净很大程度取决于依赖怎么管理。于是选中了 dia。这个库在 Flutter 圈子里不算顶流但它恰好是“纯 Dart 无代码生成 类型安全”三个特性的交集鸿蒙化适配成本极低却能带来很实在的依赖注入与工程解耦收益。这篇文章就记录了我把 dia 弄到鸿蒙 Flutter 工程里的完整过程包括环境、配置、代码、踩坑以及一个可以直接照抄的架构模板。适合正在做鸿蒙 Flutter 适配或者想在项目里引入依赖注入但不想碰 get_it 那套手动注册姿势的人。1. dia 到底是干嘛的一个“无感”的依赖注入框架1.1 为什么是 dia而不是 get_it、providerFlutter 生态里做依赖注入多数人第一个想到的是 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.getApiClient(); }注意到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 里加上 diadependencies: 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.getGreetingService(); 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; FutureUserProfile login(String username, String password) async { // 网络请求逻辑 } } class AuthRepository { AuthRepository(this._api, this._cache); final AuthApi _api; final LocalCache _cache; FutureUserProfile 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 StateLoginPage createState() _LoginPageState(); } class _LoginPageState extends StateLoginPage { late final LoginViewModel _viewModel; override void initState() { super.initState(); _viewModel DiaInjector.getLoginViewModel(); } // 页面其他逻辑 }这种方式的好处是页面不自己 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是区分泛型参数的ApiClientHttpApi和ApiClientUserApi是两个不同的 key。如果你在模块里绑定了ApiClientUserApi去解析ApiClientHttpApi自然找不到。第三步排查循环依赖。比如A构造时需要BB构造时需要A这在运行时表现为栈溢出或超时。解决办法是把其中一个依赖改为懒加载在工厂方法里改成() injector.getA()而不是直接接收参数。这种写法我见过很多团队踩坑尤其是在接缓存和配置类时特别容易出现。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 后建议马上把项目的初始化逻辑按照“模块化”整理一遍不要边迁边留一堆全局单例。等鸿蒙生态越来越成熟后面再接入其他三方库时你会发现架构的底气全都在这一步打下来了。