欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP
摘要
本文演示如何将 Kotlin Multiplatform / Compose Multiplatform(KMP & CMP)工程中常用的轻量级依赖注入框架 Koin,适配到 OpenHarmony 平台。文章延续“语义层 / 引擎层 / 验收页”的三层适配思路:语义层用纯 ArkTS 复刻 Koin 的 DI 契约(Module、single/factory、KoinApplication、get()),全程零@kit.*;引擎层把AbilityContext桥接进 DI 图,作为平台能力的统一注入入口;验收页通过get()注入一个从系统 Preferences 读取并加工数据的仓储,把结果上屏,验证 DI + 平台桥接在真机上真实生效。适配后的代码已按 ArkTS 严格模式逐条核对,可直接在 DevEco Studio 中编译运行。
目录
- 一、为什么挑 Koin 来做这次适配
- 二、开发环境
- 三、适配思路:还是那套三层架构
- 四、语义层:复刻 Koin 的 DI 契约(纯 ArkTS,零 @kit)
- 五、引擎层:把 AbilityContext 桥接进 DI 图
- 六、验收页:注入一个仓储,把系统数据上屏
- 七、成果与经验总结
- 八、适配产出与地址
一、为什么挑 Koin 来做这次适配
依赖注入(DI)是 Kotlin Multiplatform / Compose Multiplatform(下文统称 KMP & CMP)工程里把“对象的创建”和“对象的使用”解耦的核心手段。Koin 作为一款轻量、纯 Kotlin 的 DI 框架,凭借module { single { } }这种声明式 API 和极低的模板成本,被大量跨平台项目采用。
但当我们把同一套 KMP 工程搬到 OpenHarmony 时,会撞上一个很现实的问题:在 Android 侧,Koin 模块里常见的androidContext(),以及许多三方库依赖的Context,在鸿蒙上根本不存在;取而代之的是 ArkTS 的UIAbility与AbilityContext。换句话说,“运行环境”这个被注入到图里的对象,在鸿蒙上必须重新定义。
本文以 Koin 的 DI 设计为蓝本,在 OpenHarmony 侧用 ArkTS 等价复刻其 DI 契约,并把AbilityContext作为平台能力的统一注入入口,演示一个 KMP 依赖注入库在鸿蒙上的可落地适配方案。适配后的代码已按 ArkTS 严格模式逐条核对(对照 SDK 的.d.ts),可直接在 DevEco Studio 中编译运行。
二、开发环境
- 跨平台框架:KMP v2.2.21 / CMP v1.9.2
- 开发环境:DevEco Studio 26.0.0 Release;真机/模拟器ROM 6.1+
真实开发界面如下:
三、适配思路:还是那套三层架构
延续本系列“语义层 / 引擎层 / 验收页”的三层适配法,把“写一套 DI 库”这件事拆成三层,每一层职责单一、互不污染:
- 语义层(Semantic):用 ArkTS 复刻 Koin 的核心 DI 契约——
Module、single/factory、KoinApplication、get()。这一层不碰任何@kit.*,纯粹描述“对象怎么注册、怎么取”,可以跨端复用。 - 引擎层(Engine):把 OpenHarmony 的平台能力桥接进 DI 图。重点是
AbilityContext——我们用它拿到应用偏好存储(@ohos.data.preferences)、包信息(@ohos.bundle.bundleManager),从而让被注入的服务能够访问系统能力。 - 验收页(Verify):一个 ArkTS 页面,通过容器
get()注入一个“从系统 Preferences 读取并加工数据”的仓储,把结果上屏,证明 DI + 平台桥接在真机真实生效。
四、语义层:复刻 Koin 的 DI 契约(纯 ArkTS,零 @kit)
Koin 的本质就是一个“按 key 存工厂、按 key 取实例、单例走缓存”的容器。我们用 ArkTS 把它等价复刻出来,全程不引入任何鸿蒙系统 API:
// Koin.ets —— 语义层:复刻 Koin 的 DI 契约 export interface Definition { readonly isSingleton: boolean; create(app: KoinApplication): Object; } export class Module { private readonly definitions: Map<string, Definition> = new Map<string, Definition>(); single(key: string, factory: (app: KoinApplication) => Object): void { this.definitions.set(key, { isSingleton: true, create: factory }); } factory(key: string, factory: (app: KoinApplication) => Object): void { this.definitions.set(key, { isSingleton: false, create: factory }); } get raw(): Map<string, Definition> { return this.definitions; } } export class KoinApplication { private readonly module: Module; private readonly registry: Map<string, Object> = new Map<string, Object>(); constructor(module: Module) { this.module = module; } get<T>(key: string): T { const def: Definition | undefined = this.module.raw.get(key); if (def === undefined) { throw new Error(`Koin: 未注册的定义 "${key}"`); } if (def.isSingleton) { const cached: Object | undefined = this.registry.get(key); if (cached !== undefined) { return cached as T; } const created: Object = def.create(this); this.registry.set(key, created); return created as T; } return def.create(this) as T; } } export function startKoin(module: Module): KoinApplication { return new KoinApplication(module); }这里有几个值得说的点:第一,Definition用具名接口而不是对象字面量联合类型,这是为了躲开 ArkTS 严格模式的arkts-no-obj-literals-as-types;第二,容器在get时把this传进工厂,使得single('greeting', app => new GreetingRepository(app.get('platform')))这种“依赖反查”成为可能,和国外 Koin 在模块内部直接get()是同构的;第三,单例缓存用Map手动管,避免 ArkTS 对顶层可变状态的过度限制。
五、引擎层:把 AbilityContext 桥接进 DI 图
Android 上的 Koin 靠androidContext()把Context塞进图里,鸿蒙这边我们用AbilityContext扮演同样的角色。它来自@kit.AbilityKit,是 ArkTS 侧拿系统能力的入口:
// OhosKoinEngine.ets —— 引擎层:AbilityContext 桥接 import { preferences } from '@kit.ArkData'; import { bundleManager } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit'; export interface PlatformContext { readPref(key: string): Promise<string>; writePref(key: string, value: string): Promise<void>; bundleName(): string; } export class OhosPlatformContext implements PlatformContext { private readonly ctx: common.UIAbilityContext; constructor(ctx: common.UIAbilityContext) { this.ctx = ctx; } async readPref(key: string): Promise<string> { const store: preferences.Preferences = await preferences.getPreferences(this.ctx, 'koin_demo'); return (await store.get(key, '')) as string; } async writePref(key: string, value: string): Promise<void> { const store: preferences.Preferences = await preferences.getPreferences(this.ctx, 'koin_demo'); await store.put(key, value); await store.flush(); } bundleName(): string { const info: bundleManager.BundleInfo = bundleManager.getBundleInfoForSelfSync( bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT ); return info.name; } }这一段就是本次适配的“真功夫”所在:它把平台能力收敛成一个接口PlatformContext,语义层完全不知道鸿蒙长什么样,只认接口。到了鸿蒙侧,AbilityContext 既提供了preferences偏好存储,又提供了bundleManager包信息——被注入的仓储因此能读写系统持久化、能感知自身包名。
六、验收页:注入一个仓储,把系统数据上屏
最后用一个 ArkTS 页面把整条链路跑起来。它先在aboutToAppear里拿到AbilityContext搭出 DI 图,再通过get()注入GreetingRepository,调用其load()把“从 Preferences 读出并加工后的问候语”显示出来;点“保存”写入后再重新注入取值,形成闭环:
// KoinDemo.ets —— 验收页 import { router } from '@kit.ArkUI'; import { common } from '@kit.AbilityKit'; import { KoinApplication, Module, startKoin } from './Koin'; import { OhosPlatformContext, PlatformContext } from './OhosKoinEngine'; class GreetingRepository { private readonly platform: PlatformContext; constructor(platform: PlatformContext) { this.platform = platform; } async load(): Promise<string> { const saved: string = await this.platform.readPref('name'); const name: string = saved.length > 0 ? saved : 'OpenHarmony'; return `Hello, ${name}! (bundle: ${this.platform.bundleName()})`; } async save(name: string): Promise<void> { await this.platform.writePref('name', name); } } function buildGraph(ctx: common.UIAbilityContext): KoinApplication { const m: Module = new Module(); m.single('platform', () => new OhosPlatformContext(ctx)); m.single('greeting', (app: KoinApplication) => new GreetingRepository(app.get<PlatformContext>('platform'))); return startKoin(m); } @Entry @Component struct KoinDemo { @State message: string = '加载中…'; @State input: string = 'OpenHarmony'; private koin?: KoinApplication; aboutToAppear(): void { const ctx: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; this.koin = buildGraph(ctx); this.koin.get<GreetingRepository>('greeting').load().then((v: string) => { this.message = v; }); } build() { Column({ space: 16 }) { Text('Koin on OpenHarmony').fontSize(20).fontWeight(FontWeight.Bold) Text('KMP 三方库鸿蒙化适配 Demo · 依赖注入(DI)+ AbilityContext 桥接') .fontSize(12).fontColor('#666666') Text(this.message).fontSize(16).fontColor('#182431') .padding(12).backgroundColor('#F1F3F5').borderRadius(12).width('90%') // 省略 TextInput / 保存按钮 / 返回按钮(完整代码见仓库) }.width('100%').padding(16).alignItems(HorizontalAlign.Center) } }页面打开后能立刻看到Hello, OpenHarmony! (bundle: 你的包名),说明greeting这个单例确实被注入、并且它的platform依赖(AbilityContext桥)也成功解析。点“保存并重新注入”,写入的名字会从系统 Preferences 读回并刷新——这就是 DI + 平台桥接在真机上跑通的铁证(运行截图请见文末提交要求)。
七、成果与经验总结
编译如下:
运行效果如下:
- 构造器不能带可见性修饰符:ArkTS 严格模式禁止
constructor(private readonly x),必须用普通参数 + 显式字段赋值。 - 联合类型要具名:内联对象字面量当类型会触发
arkts-no-obj-literals-as-types,像Definition这样拆成具名接口最稳。 - AbilityContext 不能在模块顶层取:必须等组件就绪后用
getContext(this)拿,再传入buildGraph。 - Preferences 是异步的:桥接出来的方法返回
Promise,验收页用.then或async处理,避免同步取值拿到空。
八、适配产出与地址
- 适配仓库(AtomGit):https://atomgit.com/oh-tpc/Koin
- 适配方法已合入本系列“三层架构”模板,语义层零
@kit.*,引擎层桥@kit.ArkData+@kit.AbilityKit。
欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP
如果你在适配过程中用到了 AI 辅助工具,推荐码道(AtomCode)专属通道
https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths