Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
大家好,我是熊猫钓鱼!欢迎大家和我一起探讨技术。希望您能点赞关注,谢谢!

摘要
本文是「OpenHarmony 鸿蒙化三方库适配」延续「纯逻辑等价复刻」路线——在已交付 Decompose(组件化导航)、Essenty(生命周期/状态保持)、MVIKotlin(单向数据流)、Reaktive(响应式原语)之后,补齐「依赖注入」这一层:evant 开源的 kotlin-inject。它用编译期代码生成把 @Inject / @Component / @Module / @Provides / @Singleton / @Named 翻译成依赖图,让 KMP 业务零样板拿到注入好的实例。本文用纯 ArkTS 等价复刻同一套语义:Module(绑定集合)、Binding(工厂 + 作用域)、DiComponent(依赖图 + 解析 + 组件级单例缓存)、qualifier(字符串),不引入任何 @kit.*;并在 InjectDemo.ets 用四个按钮演示「构造注入 / 单例复用 / @Named qualifier 区分 / factory 与 singleton 引用差异」。
适配上属于「等价复刻」路线,且连引擎层都不需要,只有「语义层 + 验收页」两层——系统里没有任何东西叫 Component 或 Provider,全部用纯 ArkTS 还原。全文总结了 ArkTS 适配踩的 6 个坑(字段初始化调用方法、组件级单例语义、泛型 + Map 值类型、构造注入反向解析、=== 比较实例、静态字段自增),并与系列其它 8 个适配做了横向对比。其中关键的 ArkTS 命名细节是:依赖图类名取 DiComponent 以避开 ArkUI 的 @Component 装饰器冲突。
本适配基于 HarmonyOS SDK 6.0.0(20) + KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)开发,assembleHap 编译 BUILD SUCCESSFUL、零 ArkTS error,纯逻辑零平台依赖,模拟器即可演示、无需任何权限或硬件。
目录
- 一、为什么要适配 kotlin-inject
- 中大型 KMP 应用的依赖网;与 Dagger/Hilt 同源、编译期生成、零运行时依赖
- 适配目标:把「依赖图语义」还原成 ArkTS(@Module→Module.bind / @Inject→工厂反向 get / @Singleton→scope / @Named→qualifier)
- 二、路线取舍:等价复刻,且只有两层
- 系列两条路线回顾(真接口真实现 / 等价复刻)
- 为何不复用上游 Kotlin + KSP 源码
- 核心类型一览(Module / Binding / DiComponent / Scope / qualifier)
- 三、语义层:把依赖图契约画出来(不碰任何 @kit.*)
Module:绑定集合(bindSingleton / bindFactory / include)Binding:非泛型绑定载体(Object 承载,对外 as T 还原)DiComponent:依赖图(解析 get + 组件级单例缓存)- 构造函数注入如何表达(工厂反向 component.get,对应 @Inject constructor)
- 四、验收页:四个按钮讲清 DI 三件事
- 解析 UserService(构造注入验证)
- 检查单例复用(@Singleton)
- 检查 qualifier 区分(@Named)
- 检查 factory vs singleton(引用差异)
- 运行效果描述
- 五、开发调试
- 字段初始化调用方法、组件级单例语义、泛型+Map 值类型、构造注入反向解析、
===比较、静态字段自增
- 字段初始化调用方法、组件级单例语义、泛型+Map 值类型、构造注入反向解析、
- 六、版本与运行环境
- 适配平台、工具链、IDE、编译验证结果
- 小结
- 三层架构 + 等价复刻在纯逻辑库上的高效性;arkivanov 生态底座愈发完整
- 社区引导语与 AtomCode 专属邀请链接
一、为什么要适配 kotlin-inject
写中大型 KMP 应用,依赖关系会迅速变成一张网:页面依赖 ViewModel,ViewModel 依赖 Repository,Repository 依赖 DataSource 与 Logger……手动 new 既臃肿又难测试。
kotlin-inject 是 evant 开源的 Kotlin 依赖注入库,思路和 Dagger/Hilt 一脉相承,但编译期生成而非运行时反射,无 KAPT 性能坑、零运行时依赖,特别适合 KMP。
在 OpenHarmony 上,ArkTS 没有注解处理器(或很难接 kotlin-inject 的 KSP 生成),但我们适配的目标很明确:
把 kotlin-inject 的「依赖图语义」原样还原成 ArkTS——@Module + @Provides 变成 Module.bindXXX,@Inject constructor(...) 变成工厂函数反向 component.get(...),@Singleton / @Named 变成 scope 与 qualifier 字符串。

解析路径:get(userService) → 工厂向 DiComponent 反解 logger → 返回注入好依赖的 UserService 实例
二、路线取舍:等价复刻,且只有两层
和 MVIKotlin / Reaktive 一样,kotlin-inject 属于「等价复刻」路线:系统里没有任何东西叫 Component 或 Provider,所以连引擎层都不需要,只有「语义层 + 验收页」两层。
为什么不复用上游 Kotlin 源码?上游是 Kotlin + KSP 代码生成,要在鸿蒙跑需要
ohosArm64目标与一整套注解处理流水线,成本不可控;而 kotlin-inject 的「依赖图语义」本身就是几百行纯逻辑(绑定收集、按 qualifier 解析、组件级缓存),复刻比编译上游划算得多,还能随手丢进 Node 做离线单测。
本适配实现的核心类型:Module(绑定集合)、Provider(工厂 + 作用域)、DiComponent(依赖图 + 解析 + 单例缓存)、Scope(singleton/factory)、qualifier(字符串)。

三、语义层:把依赖图契约画出来(不碰任何 @kit.*)
核心文件 Inject.ets 定义了一组角色,一一对应 kotlin-inject。注意:依赖图类名取 DiComponent(而非 Component),是为了避开 ArkUI 的 @Component 装饰器命名冲突——这是 ArkTS 适配里一个容易踩、但很关键的命名细节。

Module —— 绑定集合(对应 @Module + @Provides)
bind(qualifier, factory, scope) 是唯一的注册入口,返回新 Module(链式组合)。两个便捷方法:bindSingleton(对应 @Singleton @Provides)、bindFactory(每次新建)。include(other) 把另一个 Module 的绑定并入,对应 @Component(modules = [A, B])。
Provider<T> —— 可被解析的绑定
持有「工厂函数 + 作用域」。关键设计:Provider 本身不缓存实例,缓存决策交给 Component——这样才能保证「单例是组件级单例,而非模块级单例」。工厂函数类型是 (component: DiComponent) => T,正是它让「构造函数注入」得以表达。
DiComponent —— 依赖图(对应 @Component 生成的实现类)
get<T>(qualifier) 是唯一的消费入口:查 Module 里有没有这个 qualifier 的绑定,没有就抛「未找到绑定」;有则看作用域——singleton 且缓存命中就返回同一对象,否则 produce 新建并(若是 singleton)写入 cache。has / registered 用于自检。
构造函数注入如何表达(对应 @Inject constructor(…))
原库里 @Inject constructor(logger: ILogger) 由 KSP 生成「先解析 logger 再 new」的代码。我们在 bindFactory('userService', c => new UserService(c.get('logger'))) 里手动写出同样的逻辑:工厂函数 c 就是「生成的构造代码」,它向 DiComponent 反向解析依赖,于是 UserService 拿到的 logger 是依赖图自动注入的。
四、验收页:四个按钮讲清 DI 三件事
InjectDemo.ets 用四个按钮把「构造注入 / 单例 / qualifier / factory」全部点亮:
- 解析 UserService:
component.get<UserService>('userService')。日志打印UserService 已注入 Console 日志器;users=Alice,Bob,Carol——证明UserService的ILogger依赖由依赖图自动注入,无需手动new ConsoleLogger()。 - 检查单例复用:连续两次
get('logger'),用===比较。日志打印两次 get(logger) 同一引用?是(@Singleton 生效)——证明组件级单例缓存生效。 - 检查 qualifier 区分:
get('logger')与get('logger:file')拿到不同实现。日志打印default.tag=Console file.tag=File 两者不同(@Named 生效)——证明 qualifier 成功区分同名类型。 - 检查 factory vs singleton:两次
get('counter')拿到不同id。日志打印两次 get(counter) id=1 / 2 不同实例(factory 每次新建)——证明非单例绑定每次新建。
跑起来:进页自动打印「依赖图已构建,注册绑定:logger、logger:file、userService、counter」;依次点四个按钮,事件日志逐条验证 DI 三件事。整条依赖注入闭环一目了然。

五、开发调试
我在Dev Eco 26最新环境下,完成本项目的代码开发界面如下:

项目编译完成:

启动运行,实践测试功能完成情况:

解析UserService(验证构造注入)如下,成功完成!

下面我们检查单例复用,@Singleton,实测如下:

OK,已生效!
项目下面我们检查qualifier区分(@Named)

也成功了!
最后:检查factory vs singleton,两种模式的引用差异如下:

以上,全部适配测试完成!
说一下在ArkTS 适配踩的坑:
- 字段初始化调用方法:ArkTS 对 struct 字段初始化器
= this.buildX()有限制,改用private component: Component = InjectDemo.createComponent()静态工厂,避免this未初始化问题。 - 组件级单例语义:第一版把缓存放在
Provider上,会导致「跨 Component 共享单例」。修正为缓存放在Component.cache,严格组件级,符合 kotlin-inject「单例存活于 Component 实例」的语义。 - 泛型 + Map 值类型:
Module.bindings用Map<string, Provider<unknown>>存异构绑定,get<T>再as T还原。ArkTS 支持泛型方法,但 Map 值必须统一为unknown才能混装不同类型 Provider。 - 构造函数注入的「反向解析」:ArkTS 没有真注解,工厂函数必须显式
c.get(...)写出依赖顺序;若漏写某个依赖,运行时才抛「未找到绑定」,所以验收页用registered先打印全部绑定自检。 ===比较实例:单例/工厂验证依赖引用同一性,ArkTS 引用类型===语义与 JS 一致,可直接用于断言。- 静态字段自增:
Counter.seq用static字段自增生成 id,ArkTS 支持 static 字段,但必须初始化(= 0)否则报状态可变性错误。
六、版本与运行环境
- 适配目标平台:HarmonyOS SDK 6.0.0(20)(API 20)
- 工具链:KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)
- IDE:DevEco Studio 26.0.0 Release
- 编译验证:
assembleHapBUILD SUCCESSFUL,零 ArkTS error
小结
在我看来,本次kotlin-inject 的适配再次验证了「三层架构 + 等价复刻」在纯逻辑类 KMP 库上的高效:几百行 ArkTS 把依赖注入(Module/Provider/Component、构造注入、@Singleton、@Named)完整还原,零平台依赖、模拟器即跑、可离线单测。
其实呢,配合 Decompose / Essenty / MVIKotlin / Reaktive,arkivanov 生态的「导航 + 生命周期 + 数据流 + 响应式 + 注入」底座在 OpenHarmony 上愈发完整。
本文完,谢谢大家阅读!欢迎点赞关注我!
欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP
AtomCode 专属邀请链接:
https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
更多推荐




所有评论(0)