Kotlin Multiplatform for OpenHarmony 实战:为 Koin 实现依赖注入适配与 AbilityContext 桥接
欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP
一、为什么挑 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
- 鸿蒙 SDK:HarmonyOS SDK 6.0.0(20)(API 20)
- 开发环境:DevEco Studio 26.0.0 Release;真机/模拟器 ROM 6.1+
征文规则推荐优先使用 HarmonyOS 7.0.0,但 KMP/CMP 鸿蒙社区工具链当前稳定版对应 6.0.0(20),因此本文按 6.0.0(20) 口径撰写,并以真实编译验证作为支撑。
三、适配思路:还是那套三层架构
延续本系列“语义层 / 引擎层 / 验收页”的三层适配法,把“写一套 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 严格模式)
- 构造器不能带可见性修饰符: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)专属通道
更多推荐


所有评论(0)