在这里插入图片描述

社区引导(开头):欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP

一、为什么挑 moko-resources 来做这次适配

moko-resources 是 Kotlin Multiplatform 生态里最常用、也最“隐形”的一个库:它让 iOS / Android / JVM 共用同一套字符串、图片、字体资源,开发者写 MR.strings.greeting、MR.images.logo,编译期自动生成各平台的资源访问代码。对“一套代码多端运行”的 KMP 工程来说,它几乎是资源层的标配。

但把这个库搬到 OpenHarmony 时,我们会发现一个现实问题:moko-resources 在 iOS/Android 上读取的是各平台“资源目录”里的编译产物(iOS 的 Localizable.strings、Android 的 strings.xml / res/drawable)。而 OpenHarmony 既没有一个叫 MR 的生成器,也没有 strings.xml 这种目录——取而代之的是 ArkTS 的 ResourceManager(@ohos.resourceManager,经 @kit.LocalizationKit 暴露)与 ImageKit 的图像解码。换句话说,“资源从哪读”这件事,在鸿蒙上必须重新定义。

本文以 moko-resources 的资源契约为蓝本,在 OpenHarmony 侧用 ArkTS 等价复刻其“按 key 取字符串 / 按 key 取图片 / 按 locale 解析”的能力,并把系统能力桥接进去,演示一个 KMP 资源库在鸿蒙上的可落地适配方案。适配后的代码已按 ArkTS 严格模式逐条核对(对照 SDK 的 .d.ts),可直接在 DevEco Studio 中编译运行。

二、开发环境

  • 跨平台框架:KMP v2.2.21 / CMP v1.9.2
  • 开发环境:DevEco Studio 26.0.0 Release;真机/模拟器 ROM 6.1+

开发本项目代码界面如下:
在这里插入图片描述

三、适配思路:还是那套三层架构

延续本系列“语义层 / 引擎层 / 验收页”的三层适配法:

  1. 语义层(Semantic):用 ArkTS 复刻 moko-resources 的核心契约——StringResource / DrawableResource(只含 key 的资源引用)、Resources 工厂方法、ResourceReader 读取接口。这一层不碰任何 @kit.*,纯粹描述“要哪个资源、怎么读”,可以跨端复用。
  2. 引擎层(Engine):把 OpenHarmony 的平台能力桥接进资源读取。重点是 ResourceManager(读字符串/图片)+ @ohos.i18n(取系统语言)+ ImageKit(把字节解码为 PixelMap)。
  3. 验收页(Verify):一个 ArkTS 页面,用 ResourceReader 按 locale 解析字符串、解码 logo 图片并上屏,点“切换语言”看到同一资源 key 在不同 locale 下取到不同值,证明适配真实生效。

四、语义层:复刻 moko-resources 的资源契约(纯 ArkTS,零 @kit)

moko-resources 的本质就是“用 key 引用资源、用 Reader 按平台读取”。我们用 ArkTS 等价复刻,全程不引入任何鸿蒙系统 API:

// MokoResources.ets —— 语义层:复刻资源契约
export interface StringResource { readonly key: string; }
export interface DrawableResource { readonly key: string; }

export class Resources {
  static string(key: string): StringResource { return { key }; }
  static drawable(key: string): DrawableResource { return { key }; }
}

export interface ResourceReader {
  getString(res: StringResource, args?: string[], locale?: string): string;
  getImage(res: DrawableResource): Object | null; // 像素图用 Object 持有,显示时再 as 成 image.PixelMap
  currentLocale(): string;
}

这里有两个要点:第一,资源描述只用 key,与 moko 生成的 MR.strings.xxx 一一对应,做到“平台无关引用”;第二,像素图在语义层只用 Object 形态持有(不绑定 @kit.ImageKit 类型),真正显示时才 as 成 image.PixelMap 交给 ArkUI Image——这避开了 ArkTS 严格模式对跨层耦合类型的限制。

五、引擎层:把 ResourceManager / i18n / ImageKit 桥接进读取

OpenHarmony 上“资源目录”的等价物是 ResourceManager。我们把它和 @ohos.i18n、ImageKit 一起收进 OhosResourceReader:

// OhosMokoResourcesEngine.ets —— 引擎层(节选)
import { i18n } from '@kit.LocalizationKit';
import { image } from '@kit.ImageKit';
import { util } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit';
import { DrawableResource, ResourceReader, StringResource } from './MokoResources';

export class OhosResourceReader implements ResourceReader {
  private readonly ctx: common.UIAbilityContext;
  constructor(ctx: common.UIAbilityContext) { this.ctx = ctx; }

  currentLocale(): string {
    // @ohos.i18n.getSystemLanguage() 返回 "zh-Hans" / "en-US" 等 BCP-47,取第一段即主语言
    const lang: string = i18n.getSystemLanguage();
    const primary: string = lang.split('-')[0];
    return primary.length > 0 ? primary : 'zh';
  }

  getString(res: StringResource, args?: string[], locale?: string): string {
    const loc: string = locale ?? this.currentLocale();
    // 演示用的内置多语言回退表(locale -> key -> value);真实工程可改为优先读 app 的 ResourceManager
    const table: Map<string, string> | undefined = BUNDLED_STRINGS.get(loc);
    let value: string | undefined = table?.get(res.key);
    if (value === undefined) {
      try { value = this.ctx.resourceManager.getStringByNameSync(res.key); } catch (e) { value = undefined; }
    }
    if (value === undefined) { return res.key; }
    if (args !== undefined && args.length > 0) {
      let out: string = value;
      for (let i = 0; i < args.length; i++) { out = out.replace('%s', args[i]); }
      return out;
    }
    return value;
  }

  getImage(res: DrawableResource): Object | null {
    try {
      const bytes: Uint8Array = new util.Base64Helper().decodeSync(LOGO_BASE64);
      const buffer: ArrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
      const source: image.ImageSource = image.createImageSource(buffer);
      const pixelMap: image.PixelMap = source.createPixelMapSync();
      source.release();
      return pixelMap;
    } catch (e) { return null; }
  }
}

这一段是本次适配的“真功夫”:ResourceManager.getStringByNameSync 是 moko-resources 在鸿蒙上的真实桥接点(对应 Android 的 strings.xml 读取),@ohos.i18n 给出默认 locale,ImageKit 把图片字节解码为 PixelMap。为了 demo 不依赖 app 资源也能跑,内置了一张 logo 的 base64 与 zh/en 多语言表作为回退——真实工程里把回退换成 ResourceManager 优先读取即可。

六、验收页:按 locale 解析字符串、解码图片并上屏

验收页在 aboutToAppear 里拿到 AbilityContext 建出 ResourceReader,立即解析 app_name / greeting / about 三个字符串并解码 logo;点“切换语言”在 zh / en 间切换,肉眼可见同一 key 取到不同文案:

// MokoResourcesDemo.ets —— 验收页(节选)
aboutToAppear(): void {
  const ctx = getContext(this) as common.UIAbilityContext;
  this.reader = new OhosResourceReader(ctx);
  this.refresh();
}
private refresh(): void {
  const r = this.reader!;
  this.locale = r.currentLocale();
  this.appName = r.getString(Resources.string('app_name'), undefined, this.locale);
  this.greeting = r.getString(Resources.string('greeting'), ['OpenHarmony'], this.locale);
  this.about = r.getString(Resources.string('about'), undefined, this.locale);
  this.logo = r.getImage(Resources.drawable('logo')) as image.PixelMap | null;
}

页面打开即看到 app_name:鸿蒙资源示例、greeting:你好,OpenHarmony! 与一张 logo 图;点“切换语言”后变为 OH Resource Demo / Hello, OpenHarmony!——这就是“多语言资源按 locale 解析”在真机上跑通的铁证(运行截图请见文末提交要求)。

七、成果和经验总结

编译build如下:
在这里插入图片描述
运行如下:

在这里插入图片描述
在这里插入图片描述
可以看到已成功实现中英文之间的无缝切换。

项目遇到了一些问题,整理如下:

  1. 构造器不能带可见性修饰符:constructor(private readonly x) 被禁,必须用普通参数 + 显式字段赋值。
  2. @ohos.i18n 经 @kit.LocalizationKit 暴露:直接用 import { i18n } from '@kit.LocalizationKit',不要猜 @ohos.i18n 的裸路径。
  3. 图片同步解码用 createPixelMapSync:异步 createPixelMap() 返回 Promise,会破坏 getImage 的同步契约;用同步变体最干净,且用完 source.release() 释放句柄。
  4. base64 解码是实例方法:new util.Base64Helper().decodeSync(str),不是静态 Base64Helper.decode。
  5. 像素图跨层用 Object 持有:语义层不引 @kit.ImageKit,显示时再 as。

八、适配产出与地址

  • 适配仓库(AtomGit):https://atomgit.com/oh-tpc/moko-resources
  • 适配方法已合入本系列“三层架构”模板,语义层零 @kit.*,引擎层桥 @kit.LocalizationKit + @kit.ImageKit + @kit.ArkTS。

社区引导(结尾):欢迎加入KMP&CMP 鸿蒙社区: https://atomgit.com/CPF-KMP-CMP

如果你在适配过程中用到了 AI 辅助工具,推荐码道(AtomCode)专属通道:

https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths

Logo

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

更多推荐