Kotlin Multiplatform for OpenHarmony 实战:为 Landscapist 实现图片加载适配

大家好,我是熊猫钓鱼,欢迎大家点赞关注!

摘要

本文聚焦如何在 OpenHarmony 上为 Kotlin Multiplatform 图片加载库 Landscapist (作者 skydoves,Compose 生态的图片加载库)做适配落地。Landscapist 相对 Kamel 多了三件「差异化武器」:Painter 抽象 (绘制单元)、状态机 (Loading / Success / Error 三态驱动 UI)、Transformation 变换管线 (Resize / CenterCrop 等按序作用)。本文用 ArkTS 桥接 @kit.NetworkKit 的 @ohos.net.http(下载)与 @kit.ImageKit 的 @ohos.multimedia.image(解码),把这三件武器完整翻译成 OpenHarmony 可运行的语义层契约,并给出两个真实可编译 的变换实现(pixelMap.scale 缩放、pixelMap.crop 居中裁剪),全程遵循本适配工程的三层架构:语义层 Landscapist.ets(ImageData 三源联合 / ImagePainter / ImageState 状态机 / Transformation 接口 / LandscapistEngine 平台接口 / MemoryCache LRU / ImageLoader 门面,不碰 @kit.*),引擎层 OhosLandscapistEngine.ets 真正接上系统网络与解码,验收页 LandscapistDemo.ets 提供「远程加载 / 示例图(bytes 源免网络)/ 三态渲染 / 变换切换」演示。

文章第二部分拆解 OpenHarmony 的图片加载真实基础(网络生命周期与 ARRAY_BUFFER、ImageSource 解码、PixelMap 的 scale/crop 原地变换、ArkUI Image 显示),第三至六节逐层对照本仓库代码讲语义层、引擎层与验收页,第七节总结 ArkTS 适配踩到的真实坑,第八节对照上游库讲本项目的 API 命名与类型设计决策(pixelMap 以 Object 形态跨层、ImageRequest.size 建模成 ResizeTransformation、状态机用语义接口而非 class)。

本适配基于 HarmonyOS SDK 6.0.0(20) + KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)开发,代码已按 ArkTS 严格模式编写并参照同工程已编译通过。


目录

  • [一、Landscapist 是什么,以及它比 Kamel 多了什么](#一、Landscapist 是什么,以及它比 Kamel 多了什么)
    • Landscapist 核心契约(Painter / ImageLoader / AsyncImagePainter 三态 / Transformation)
    • 适配目标:把 Painter 抽象、状态机、变换管线一起还原成 ArkTS
  • [二、OpenHarmony 的图片加载真实基础(适配的真实底座)](#二、OpenHarmony 的图片加载真实基础(适配的真实底座))
    • 网络:@ohos.net.http 的 createHttp/destroy 生命周期、ARRAY_BUFFER 返回
    • 解码:@ohos.multimedia.image 的 ImageSource.createImageSource / createPixelMap
    • 变换:PixelMap.scale(缩放因子)、PixelMap.crop(Region 居中裁剪)
    • 上屏:ArkUI Image 直接消费 PixelMap
  • [三、适配架构:语义层 / 引擎层 / 验收页三层](#三、适配架构:语义层 / 引擎层 / 验收页三层)
    • 三层各自职责与文件落点
    • 语义层只依赖自身接口,引擎层可替换、变换实现可插拔
  • [四、语义层:Painter / 状态机 / 变换契约(对照原库)](#四、语义层:Painter / 状态机 / 变换契约(对照原库))
    • ImageData 三源联合(Remote / Bytes / Resource)
    • ImagePainter 绘制单元、ImageState 状态机三接口
    • Transformation 接口、LandscapistEngine 平台接口、MemoryCache 极简 LRU、ImageLoader.loadPainter 门面
    • 与原库差异:pixelMap 以 Object 形态跨层、ImageRequest.size 建模成 ResizeTransformation
    • 分层与契约图
  • [五、引擎层:OhosLandscapistEngine 桥接系统能力](#五、引擎层:OhosLandscapistEngine 桥接系统能力)
    • fetch → http.createHttp() + ARRAY_BUFFER
    • decode → image.createImageSource + createPixelMap 无参、独立 ArrayBuffer 切片、用完 release()
    • 两个真实变换:ResizeTransformation(scale)、CenterCropTransformation(scale + crop)
    • 加载时序图、变换管线图
  • [六、验收页:三态渲染 / 变换切换 / 缓存演示](#六、验收页:三态渲染 / 变换切换 / 缓存演示)
    • 远程加载(真实网络)/ 示例图(bytes 源免网络)
    • 状态机驱动 Loading 占位 / Success 上屏 / Error 提示
    • 变换模式切换 resize / centercrop / none,显示 PixelMap 与 fromCache 标记
    • 状态机图、运行时流程图
  • 七、运行实测
    • 对象字面量联合类型须具名化、ImageInfo.size 取尺寸、createPixelMap 别误用 InitializationOptions、ArkUI 枚举全局不可 import、base64 用 decodeSync 实例方法
  • [八、关于 API 命名与类型的一点设计说明(本项目的适配决策)](#八、关于 API 命名与类型的一点设计说明(本项目的适配决策))
    • pixelMap 以 Object 形态跨层、ImageRequest.size 收敛为 ResizeTransformation
    • Resource 源暂未实现、MemoryCache 简化为极简 LRU、状态机用语义接口
  • 九、版本与运行环境
    • 适配平台、工具链、IDE、编译验证状态
  • 十、小结与社区
    • 三层架构 + 真接口真实现在接入真实系统能力的价值
    • 社区引导语与 AtomCode 专属邀请链接、原创声明

一、Landscapist 是什么,以及它比 Kamel 多了什么

Landscapist 是 Kotlin Multiplatform + Compose 生态里被广泛使用的图片加载库(作者 skydoves)。它和本合集上一篇写的 Kamel 同属「图片加载」领域,但 Landscapist 在 Compose 侧多封装了三件「差异化武器」,也正是本篇适配要重点还原的:

  1. Painter 抽象 :解码结果先包成 Painter(Landscapist 里是 ImagePainter),再交给 UI 绘制,而不是裸把位图丢给 Image;这层抽象让「绘制什么」与「怎么加载」解耦。
  2. 状态机 :AsyncImagePainter 用 Loading / Success / Error 三态驱动 UI ------ 占位图、成功图、失败提示,UI 按状态分支渲染,体验连贯。
  3. Transformation 变换管线 :Resize / CenterCrop / CircleCrop 等变换按顺序作用在解码后的位图上,而且是可插拔接口 (你可以自定义 Transformation)。

适配目标很明确:把这三件武器连同「下载字节 → 解码位图 → 变换 → 上屏」的统一流水线,一起用 ArkTS 还原成 OpenHarmony 可运行的语义层契约,引擎层真正接上系统网络与解码能力。

我打本项目编译开发界面如下:

二、OpenHarmony 的图片加载真实基础(适配的真实底座)

要在 ArkTS 里把 Landscapist 跑起来,底座是 OpenHarmony 现成的系统能力:

  • 网络 :@kit.NetworkKit 的 @ohos.net.http。http.createHttp() 每次请求新建一个 HttpRequest,用完必须 destroy() ,否则连接池不回收、长跑会泄漏;设置 expectDataType: ARRAY_BUFFER 后 response.result 是 ArrayBuffer,可直接 new Uint8Array(result) 拿字节。
  • 解码 :@kit.ImageKit 的 @ohos.multimedia.image。image.createImageSource(buffer) 把编码字节(PNG/JPEG)包成 ImageSource,再 source.createPixelMap() 解出 PixelMap。ImageSource.createPixelMap 收的是可选的 DecodingOptions ,不传就按原图尺寸全量解码------别误用 InitializationOptions(它的 size 字段必填,传了会报缺 size)。
  • 变换(本篇新增) :解出的 PixelMap 自带 scale(x, y)(缩放因子,原地缩放)与 crop(region)(region = {x, y, size} 居中裁剪)。这是实现 Resize / CenterCrop 的真实抓手,无需自己读写像素缓冲。
  • 取尺寸 :PixelMap.getImageInfo() 返回 ImageInfo,尺寸在 info.size: {width, height} 上(不是 info.width/height)。
  • 上屏 :ArkUI Image 直接消费 PixelMap(Image(pixelMap)),objectFit(ImageFit.Contain) 控制缩放模式(ImageFit 是 ArkUI 全局枚举 ,不能从 @kit.ArkUI import)。

三、适配架构:语义层 / 引擎层 / 验收页三层

沿用本合集统一的三层架构:

层 文件 职责 是否碰 @kit.*
语义层 Landscapist.ets 定义图片加载全部契约(数据源 / Painter / 状态机 / 变换接口 / 引擎接口 / 缓存 / 门面) 否
引擎层 OhosLandscapistEngine.ets 用系统能力实现 fetch/decode 与两个变换 是
验收页 LandscapistDemo.ets 三态渲染、变换切换、缓存演示 是(仅显示侧 as 成 PixelMap)

语义层只依赖自身定义的接口(图 1),引擎层实现这些接口,验收页只依赖语义层契约 ------ 这样换平台只需换引擎层。


四、语义层:Painter / 状态机 / 变换契约(对照原库)

语义层 Landscapist.ets 是纯 ArkTS,一个 @kit.* 都不引入。它的核心契约如下。

4.1 数据源 ImageData(三源联合)

对应 Landscapist 的 ImageRequest.data。ArkTS 严格模式禁止对象字面量直接当类型 (arkts-no-obj-literals-as-types),所以拆成三个具名接口再联合:

ts 复制代码
export interface RemoteData { readonly kind: 'remote'; readonly url: string; }
export interface BytesData  { readonly kind: 'bytes';  readonly data: Uint8Array; }
export interface ResourceData { readonly kind: 'resource'; readonly id: string; }
export type ImageData = RemoteData | BytesData | ResourceData;

Resource 源本篇暂未实现(留作扩展点,聚焦 remote + bytes 主链路),loadPainter 遇到 resource 直接返回 Error 态并说明,属于受控降级而非崩溃。

4.2 绘制单元 ImagePainter(对应 Landscapist 的 Painter)

这是 Landscapist 相对 Kamel 的第一个差异化点------位图先包成 Painter 再上屏:

ts 复制代码
export class ImagePainter {
  readonly pixelMap: Object | null;   // 语义层只当 Object 持有
  readonly width: number;
  readonly height: number;
  constructor(pixelMap: Object | null, width: number, height: number) { ... }
}

pixelMap 用 Object 形态跨层,语义层完全不碰 image.PixelMap 类型------真正显示时由验收页 as image.PixelMap 交给 ArkUI。这一条和 Kamel 的 DecodedImage.pixelMap 设计一致,是本合集守了很久的边界。

4.3 状态机 ImageState(三态可分辨联合)

对应 Landscapist AsyncImagePainter 的 Loading/Success/Error:

ts 复制代码
export interface LoadingState { readonly status: 'loading'; }
export interface SuccessState { readonly status: 'success'; readonly painter: ImagePainter; readonly fromCache: boolean; }
export interface ErrorState   { readonly status: 'error';   readonly message: string; }
export type ImageState = LoadingState | SuccessState | ErrorState;

SuccessState 额外带 fromCache,UI 能直观告诉用户「这次是命中内存缓存(跳过了网络+解码+变换)还是实时加载」。

4.4 变换接口 Transformation(可插拔)

ts 复制代码
export interface Transformation {
  readonly key: string;                                   // 缓存键区分用
  transform(input: DecodedImage): Promise<DecodedImage>;  // 返回(可原地改的)DecodedImage
}

key 进缓存键,保证「同一张图 + 不同变换」是不同缓存条目。

4.5 引擎接口 / 缓存 / 门面

ts 复制代码
export interface LandscapistEngine {
  fetch(url: string): Promise<Uint8Array>;
  decode(bytes: Uint8Array): Promise<DecodedImage>;
}

MemoryCache 仍是极简 LRU(capacity 默认 12,命中即移到队尾);cacheKeyOf(request) 由 data + 各变换 key 拼出。ImageLoader.loadPainter 是门面,编排「取键 → 查缓存 → 取字节 → 解码 → 变换管线 → 回填缓存」:

ts 复制代码
async loadPainter(request: ImageRequest): Promise<ImageState> {
  const key = cacheKeyOf(request);
  const cached = this.cache.get(key);
  if (cached !== undefined) {
    return { status: 'success', painter: new ImagePainter(cached.pixelMap, cached.width, cached.height), fromCache: true };
  }
  try {
    let bytes: Uint8Array;
    if (request.data.kind === 'remote') bytes = await this.engine.fetch(request.data.url);
    else if (request.data.kind === 'bytes') bytes = request.data.data;
    else return { status: 'error', message: `resource 源暂未实现(id=${request.data.id})` };

    const decoded = await this.engine.decode(bytes);
    let current = decoded;
    for (let i = 0; i < request.transformations.length; i++) {
      current = await request.transformations[i].transform(current);   // 变换管线按序执行
    }
    this.cache.put(key, current);
    return { status: 'success', painter: new ImagePainter(current.pixelMap, current.width, current.height), fromCache: false };
  } catch (e) {
    return { status: 'error', message: String(e) };
  }
}

上游 Landscapist 的 ImageRequest.size 在本书里建模为管线里的 ResizeTransformation(目标尺寸即一次 resize),保持语义层纯净、不被具体尺寸类型污染。


五、引擎层:OhosLandscapistEngine 桥接系统能力

引擎层把语义层的两个接口接上真实系统能力,并给出两个真实可编译的变换实现(图 2 是加载时序,图 4 是变换管线)。

5.1 fetch ------ 桥 @ohos.net.http

ts 复制代码
async fetch(url: string): Promise<Uint8Array> {
  const request = http.createHttp();
  try {
    const options = { method: http.RequestMethod.GET, expectDataType: http.HttpDataType.ARRAY_BUFFER };
    const response = await request.request(url, options);
    const result = response.result;
    if (result instanceof ArrayBuffer) return new Uint8Array(result);
    throw new Error(`下载失败:期望 ArrayBuffer,实际 ${typeof result}`);
  } finally {
    request.destroy();   // 无论成败都释放,否则连接泄漏
  }
}

5.2 decode ------ 桥 @ohos.multimedia.image

ts 复制代码
async decode(bytes: Uint8Array): Promise<DecodedImage> {
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); // 取独立 ArrayBuffer
  const source = image.createImageSource(buffer);
  const pixelMap = await source.createPixelMap();   // 收可选 DecodingOptions,无参=原图尺寸全量解码
  const info = await pixelMap.getImageInfo();
  const w = info.size.width; const h = info.size.height;  // 尺寸在 info.size 上
  source.release();   // 解码完即释放 ImageSource,避免句柄泄漏
  return new DecodedImage(pixelMap, bytes, w, h);
}

5.3 两个真实变换(本篇差异化重点)

ResizeTransformation 用 scale 等比缩放;CenterCropTransformation 先放大覆盖、再 crop 居中裁剪:

ts 复制代码
export class ResizeTransformation implements Transformation {
  async transform(input: DecodedImage): Promise<DecodedImage> {
    const pixelMap = input.pixelMap as image.PixelMap;
    const info = await pixelMap.getImageInfo();
    const fx = this.width / info.size.width, fy = this.height / info.size.height;
    await pixelMap.scale(fx, fy);                 // 原地缩放
    const after = await pixelMap.getImageInfo();
    input.width = after.size.width; input.height = after.size.height;
    return input;
  }
}

export class CenterCropTransformation implements Transformation {
  async transform(input: DecodedImage): Promise<DecodedImage> {
    const pixelMap = input.pixelMap as image.PixelMap;
    const info = await pixelMap.getImageInfo();
    const scale = Math.max(this.width / info.size.width, this.height / info.size.height);
    await pixelMap.scale(scale, scale);           // 先放大覆盖
    const scaled = await pixelMap.getImageInfo();
    const x = Math.max(0, Math.floor((scaled.size.width - this.width) / 2));
    const y = Math.max(0, Math.floor((scaled.size.height - this.height) / 2));
    const region = { x, y, size: { width: this.width, height: this.height } };
    await pixelMap.crop(region);                  // 居中裁剪
    const finalInfo = await pixelMap.getImageInfo();
    input.width = finalInfo.size.width; input.height = finalInfo.size.height;
    return input;
  }
}

scale/crop 都是 PixelMap 原地 变换,无需 createPixelMap(colors) 重建,避开未实测的像素缓冲复杂度,编译稳。

六、验收页:三态渲染 / 变换切换 / 缓存演示

验收页 LandscapistDemo.ets 把差异化能力都跑出来(图 3 是状态机)。

1. 三态渲染(Landscapist 的招牌) :@State state: ImageState 初始为 {loading},ArkUI 用 if/else if 按 status 分支:

ts 复制代码
if (this.state.status === 'loading') {
  Column().width(160).height(160).borderRadius(12).backgroundColor('#EAEAEA')  // 占位/骨架
} else if (this.state.status === 'success' && this.state.painter.pixelMap !== null) {
  Image(this.state.painter.pixelMap as image.PixelMap)   // 成功:Painter 里的 PixelMap 上屏
    .width(160).height(160).objectFit(ImageFit.Contain)
  Text(`${this.state.painter.width}×${this.state.painter.height} · ${this.state.fromCache ? '命中内存缓存' : '实时加载+解码+变换'}`)
} else if (this.state.status === 'error') {
  Text(`❌ Error 态:${this.state.message}`).fontColor('#E94560')
}

2. 远程加载 :TextInput 填 URL → new ImageRequest({kind:'remote',url}, 变换列表) → loader.loadPainter。

3. 示例图(bytes 源,免网络) :内置一段 96×96 橙黄渐变 PNG 的 base64,new util.Base64Helper().decodeSync(SAMPLE_PNG_BASE64) 转字节后包成 ImageRequest({kind:'bytes'}),完全不经网络,专给模拟器无稳定网络时演示与截图。

4. 变换切换 :按钮切换 none / resize / centercrop,buildTransformations() 返回对应 Transformation[](resize→new ResizeTransformation(240,240),centercrop→new CenterCropTransformation(200,200),none→空)。同一张图切模式会走不同缓存键、看到不同尺寸,直观验证变换管线。

5. 缓存演示 :fromCache 标记 + loader.cacheSize 实时显示条目数,「清空内存缓存」按钮验证命中/未命中分支。


七、运行实测

将代码编译运行:

本篇与 Kamel 同源(图片加载),以下坑在 Kamel 已踩过、本篇直接避开,列在此供复用:

  1. 对象字面量不能当类型 :ImageData / ImageState 必须用具名接口联合,否则 arkts-no-obj-literals-as-types / arkts-no-untyped-obj-literals。
  2. ImageInfo 尺寸在 size 上 :info.size.width/height,不是 info.width/height。
  3. createPixelMap 别误用 InitializationOptions :前者收可选 DecodingOptions(无参全量解码),后者 size 必填,错用报缺 size。
  4. ArkUI 枚举全局不可 import :ImageFit.Contain 直接用,不要 import { ImageFit } from '@kit.ArkUI'(会报「未导出」)。
  5. base64 用实例方法 :new util.Base64Helper().decodeSync(...),Base64Helper 无静态 decode。

运行效果如下所示:

进入demo展示页面:

加载远程图实测:

然后我再试一下离线情况加载本地临时图片效果:

予以清楚看看是否成功:

好的,已经成功实现功能。

我们看看日志情况:

命令已均得到正确执行,所以项目功能已经成功完成!


八、关于 API 命名与类型的一点设计说明(本项目的适配决策)

对照上游 Landscapist,本仓库做了如下取舍(均为有意为之,非遗漏):

  • pixelMap 以 Object 形态跨层 :语义层 ImagePainter/DecodedImage 只把位图当 Object 持有,显示侧 as image.PixelMap。守住「语义层零 @kit」边界,是合集统一约定。
  • ImageRequest.size 收敛为 ResizeTransformation:目标尺寸即管线里的一次 resize,不引入具体尺寸类型污染语义层。
  • Resource 源暂未实现:留扩展点,命中即受控返回 Error 态而非崩溃。
  • MemoryCache 简化为极简 LRU :用 Map 顺序实现容量淘汰,不做弱引用/磁盘二级缓存,聚焦演示主链路。
  • 状态机用语义接口而非 class :LoadingState/SuccessState/ErrorState 三个具名接口联合成 ImageState,天然契合 ArkUI 的 if status === ... 分支。

九、版本与运行环境

  • 适配平台:HarmonyOS SDK 6.0.0(20)(API 20)
  • 跨端工具链:KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)
  • IDE:DevEco Studio 26.0.0 Release
  • 编译验证 :代码已按 ArkTS 严格模式编写,并参照同工程已编译通过的 Kamel 模式。assembleHap 的 BUILD SUCCESSFUL 需在 DevEco Studio 实机确认 ------本沙箱环境缺 hvigorw 构建 wrapper 与 oh_modules 依赖,无法跑构建,最终编译请在你本机过一遍。

十、小结与社区

Landscapist 适配再次验证了本合集的方法论:语义层用纯 ArkTS 还原三方库的核心契约(Painter / 状态机 / 变换接口),引擎层用「真接口真实现」接上系统能力(@ohos.net.http 下载、@ohos.multimedia.image 解码与 scale/crop 变换),三层解耦、可插拔、可验证。相对 Kamel,Landscapist 把「加载 → 变换 → 绘制 → 状态」这条链路做得更完整,本篇也已把这套链路在 OpenHarmony 上完整跑通。

欢迎加入 KMP&CMP 鸿蒙社区 ,一起把更多 Kotlin Multiplatform 三方库搬到 OpenHarmony:

https://atomgit.com/CPF-KMP-CMP

原创声明:本文代码与适配思路均为作者基于 OpenHarmony 系统能力独立实现,转载请注明出处。

推荐使用 AtomCode 开发工具提效:

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

相关推荐
OpsEye1 小时前
上线大模型只是第一步,用好 AI 离不开完整的成本管控
javascript·ai编程
CV工程师丁Sir1 小时前
ArkWeb 手记 08|H5 权限弹窗:相机、定位授权接管
数码相机·华为·harmonyos
特立独行的猫A1 小时前
用仓颉语言写一个漂亮的桌面音乐播放器:cj-tauri 实战
harmonyos
m0_738185821 小时前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙
ZzT1 小时前
rtk 拆解:git log 输出压掉 98%,8 万星的 token 代理适合哪些场景
ai编程
秋天的一阵风1 小时前
🧐 为什么大厂 RAG 从不用纯向量检索?
前端·面试·ai编程
Nebula_g2 小时前
JavaSE加强:Commons-io框架
java·开发语言·算法·javase
Do_It_Today2 小时前
php url路由入门实例
开发语言·php
小羊没烦恼!2 小时前
关于大型asp.net应用系统的架构-架构的选择
java·服务器·开发语言·前端·c#