欢迎加入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 库”这件事拆成三层,每一层职责单一、互不污染:

  1. 语义层(Semantic):用 ArkTS 复刻 Koin 的核心 DI 契约——Module、single / factory、KoinApplication、get()。这一层不碰任何 @kit.*,纯粹描述“对象怎么注册、怎么取”,可以跨端复用。
  2. 引擎层(Engine):把 OpenHarmony 的平台能力桥接进 DI 图。重点是 AbilityContext——我们用它拿到应用偏好存储(@ohos.data.preferences)、包信息(@ohos.bundle.bundleManager),从而让被注入的服务能够访问系统能力。
  3. 验收页(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 严格模式)

  1. 构造器不能带可见性修饰符:ArkTS 严格模式禁止 constructor(private readonly x),必须用普通参数 + 显式字段赋值。
  2. 联合类型要具名:内联对象字面量当类型会触发 arkts-no-obj-literals-as-types,像 Definition 这样拆成具名接口最稳。
  3. AbilityContext 不能在模块顶层取:必须等组件就绪后用 getContext(this) 拿,再传入 buildGraph。
  4. 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)专属通道

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐