三方库适配征文 · 第二批 · 上游 ZacSweers/Metro 1.5.0
适配代码:https://atomgit.com/wdracky/kmp-ohos-adapters/tree/main/metro

摘要

本文记录 ZacSweers/Metro 1.5.0 在 OpenHarmony 上的适配实践。Metro 是 KMP 生态的编译期依赖注入框架,依赖 KSP 生成依赖图实现类,而鸿蒙侧没有 KSP。适配的核心洞察是:Metro 的生成物本质是一张显式绑定表(key → provider,单例带缓存),因此可以用手写 MetroGraphBuilder 装配的方式语义复刻,实现运行时零反射的依赖注入。文章给出 DevEco Studio 26 + Kotlin 2.1.x 的版本组合、三层架构映射(Metro 上游 Kotlin 层 / 鸿蒙语义层 / 鸿蒙引擎层)、循环依赖检测与作用域链两个关键设计,并整理踩坑记录与 6 条 FAQ,供同类编译期框架的鸿蒙适配参考。

目录

一、Metro 是什么

Metro 是 ZacSweers 的 KMP 编译期依赖注入框架:KSP 扫描 @DependencyGraph/@Provides/@SingleIn,生成依赖图实现类——零反射、增量编译友好。2025-2026 年 KMP 社区最热门的 Dagger 替代。

二、环境搭建

适配与验证基于以下版本组合,建议严格对齐,避免 KSP / 编译器版本错配导致的生成失败:

组件版本说明
DevEco Studio26(DevEco 26)鸿蒙官方 IDE,本文所有 ArkTS 侧代码与真机/模拟器调试均在此版本完成
HarmonyOS SDK随 DevEco 26 配套(API 20+)使用 IDE 内置 SDK Manager 安装
Kotlin2.1.xKMP 侧编译与 Metro 注解处理
Kotlin Multiplatform (KMP)2.1.x与 Kotlin 版本保持一致
KMP for OpenHarmony 插件与 KMP 2.1.x 匹配的 OH 适配版本负责 Kotlin → ArkTS 产物桥接
Metro1.5.0上游适配目标版本
KSP与 Kotlin 2.1.x 对应Metro 编译期代码生成依赖

开发界面如下:
在这里插入图片描述

搭建步骤与官方指引:

  • DevEco Studio 26 下载与安装:https://developer.huawei.com/consumer/cn/deveco-studio/
  • HarmonyOS SDK 与 API 版本管理:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/ide-sdk-manager
  • Kotlin Multiplatform 官方文档(版本与 Gradle 配置):https://kotlinlang.org/docs/multiplatform.html
  • KMP for OpenHarmony 环境配置与插件说明:https://atomgit.com/CPF-KMP-CMP
  • Metro 上游仓库与版本发布记录:https://github.com/ZacSweers/Metro

提示:DevEco 26 对 ArkTS 语法校验更严格,若出现 arkts-no-any-unknown 类报错,请优先检查绑定表是否使用了 any,改用显式泛型或字符串 key 方案。

二、适配路线与核心难题

Metro 的价值在编译期代码生成,而鸿蒙没有 KSP。适配的第一步是想清楚:生成物是什么?

Metro 生成的图实现类,本质是一张显式绑定表:key → provider(构造调用),单例带缓存。鸿蒙的等价物就是手写这张表:

const graph = new MetroGraphBuilder()
  .singleton<Logger>('Logger', () => new Logger())
  .singleton<Clock>('Clock', () => new Clock())
  .provider<UserRepository>('UserRepository',
    () => new UserRepository(graph.get<Logger>('Logger'), graph.get<Clock>('Clock')))
  .build();

这正是 Metro 生成的代码逐行展开后的样子——显式图装配 = 生成代码的等价物。

整体适配路线如下:

没有

Metro 上游 Kotlin 源码

KSP 扫描注解

生成依赖图实现类

鸿蒙侧有 KSP 吗

提取生成物本质:显式绑定表

手写 MetroGraphBuilder 装配

运行时解析与单例缓存

ArkTS 侧零反射依赖注入

三、三层架构

Metro(Kotlin)鸿蒙语义层鸿蒙引擎层
Provider<T>type Provider<T> = () => T—
@SingleIn(AppScope)builder.singleton(key, provider)—
@Provides(无作用域)builder.provider(key, provider)—
@DependencyGraph(AppScope)DependencyGraphOhosMetroRuntime.configure()
@DependencyGraph(ActivityScope, parent)ScopedGraph(parent 链)createActivityScope()
graph.create<T>()graph.get<T>(key)metroGet<T>()
编译期循环依赖诊断运行时 MetroError(cycle)—

三层职责划分如下:

鸿蒙引擎层

鸿蒙语义层

Metro 上游 Kotlin 层

@DependencyGraph

@Provides / @SingleIn

KSP 代码生成

DependencyGraph / ScopedGraph

builder.singleton / builder.provider

Provider 类型别名

OhosMetroRuntime.configure

createActivityScope

metroGet 取用入口

四、三个关键设计

1. 循环依赖检测。上游在编译期诊断;鸿蒙在解析时用解析栈跟踪:

if (this.resolving.indexOf(key) >= 0) {
  throw new MetroError('cycle', '循环依赖检测:' + this.resolving.join(' -> ') + ' -> ' + key);
}

A→B→A 立即抛出,错误信息带完整解析路径。

2. 作用域链。ScopedGraph 子图未命中向上查找父图,对应 @SingleIn(ActivityScope) 依赖 AppScope 绑定;绑定惰性 build,避免空图。

3. 注解文档化。@Injectable→显式绑定、@Inject→graph.get()——把 Metro 注解语义固化为 ArkTS 侧的编写规范(MetroInjectionAnnotations.USAGE),团队照表施工即可。

循环依赖检测与作用域链查找的流程如下:

是

否

是

否

是

否

graph.get(key) 请求解析

key 已在解析栈中

抛出 MetroError(cycle) 并输出解析路径

压入解析栈

当前图已注册该 key

调用 provider 构造实例

存在 parent 图

向上委托父图解析

抛出未绑定错误

单例写入缓存

弹出解析栈并返回实例

五、踩坑记录

  1. 泛型 key 的擦除:ArkTS 泛型在运行时不保留,绑定表用字符串 key('Logger')——上游用类型做 key,鸿蒙用「类型名」做 key,取用时 metroGet<Logger>(graph, 'Logger') 显式声明,类型安全不丢。
  2. provider 里的 this 引用:UserRepository 的 provider 引用 graph 自身,ArkTS 要求先声明后使用——用 graph.get() 在 provider 内部惰性解析,构造顺序天然正确。
  3. ScopedGraph 的惰性装配:最初在构造时就 build 空图,导致后注册的绑定丢失——改为首次解析时 ensureGraph()。

六、FAQ

Q1:DevEco 26 下 KMP 插件加载失败怎么办?
先确认 IDE 版本与插件版本匹配,再检查 Settings → Plugins 中 KMP for OpenHarmony 是否启用;若仍失败,清理 ~/.gradle/caches 与项目 .hvigor 缓存后重新 Sync。

Q2:Kotlin 与 KSP 版本必须完全一致吗?
KSP 版本号形如 2.1.x-1.0.x,前半段必须与 Kotlin 版本严格对应,否则 Metro 的符号处理会直接报错。

Q3:为什么绑定表要用字符串 key,而不是类型 key?
ArkTS 运行时泛型被擦除,无法用类型做 Map 的 key。用「类型名」字符串做 key,再在取用处 metroGet<Logger>(graph, 'Logger') 显式声明类型,兼顾运行时可查与编译期类型安全。

Q4:循环依赖为什么不在编译期报错?
鸿蒙侧没有 KSP,无法在编译期构建完整依赖图,因此改为运行时解析栈检测,错误信息会带上完整解析路径,定位成本与编译期诊断接近。

Q5:ScopedGraph 子图拿不到父图绑定?
确认创建子图时传入了 parent,且父图已完成绑定注册;子图采用惰性装配,首次解析时才 build,构造阶段不要提前调用 ensureGraph()。

Q6:DevEco 26 真机调试时 DI 初始化时机?
建议在 AbilityStage.onCreate 或 EntryAbility.onCreate 中调用 OhosMetroRuntime.configure(),早于任何页面与 Service 的依赖解析。

六、总结

Metro 的鸿蒙适配证明了「编译期框架也可以语义复刻」:生成物的本质是显式绑定表,把 KSP 的工作变成「照表施工」,运行时零反射、循环可检测、作用域可继承。本系列已有 kotlin-inject/Koin/Kodein-DI 三个 DI 适配,Metro 是第四块拼图,也补上了「多作用域」语义。


系列导读:本文为「Kotlin Multiplatform for OpenHarmony 实战」第 34 篇。全部代码与博客见 https://atomgit.com/wdracky/kmp-ohos-adapters 。

欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP

【AI 工具推荐】AtomCode 专属邀请链接:https://atomgit.com/dashboard/atomcode?utm_source=av&isLogin=99

一句话总结:鸿蒙没有 KSP,但不需要 KSP——把 Metro 的编译期生成物还原成一张显式绑定表,用 MetroGraphBuilder 手写装配,就能在 ArkTS 侧拿到零反射、可检测循环、可继承作用域的依赖注入能力。适配的关键不在「移植代码生成器」,而在「读懂生成物、复刻其语义」。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐