☰
Kotlin Multiplatform for OpenHarmony 实战:为 Landscapist 实现图片加载适配
2026/10/9 15:41:12 网站建设 项目流程

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

摘要

本文聚焦如何在 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平台接口 /MemoryCacheLRU /ImageLoader门面,不碰@kit.*),引擎层OhosLandscapistEngine.ets真正接上系统网络与解码,验收页LandscapistDemo.ets提供「远程加载 / 示例图(bytes 源免网络)/ 三态渲染 / 变换切换」演示。

文章第二部分拆解 OpenHarmony 的图片加载真实基础(网络生命周期与ARRAY_BUFFER、ImageSource 解码、PixelMap的scale/crop原地变换、ArkUIImage显示),第三至六节逐层对照本仓库代码讲语义层、引擎层与验收页,第七节总结 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 核心契约(Painter / ImageLoader / AsyncImagePainter 三态 / Transformation)
    • 适配目标:把 Painter 抽象、状态机、变换管线一起还原成 ArkTS
  • 二、OpenHarmony 的图片加载真实基础(适配的真实底座)
    • 网络:@ohos.net.http的 createHttp/destroy 生命周期、ARRAY_BUFFER 返回
    • 解码:@ohos.multimedia.image的 ImageSource.createImageSource / createPixelMap
    • 变换:PixelMap.scale(缩放因子)、PixelMap.crop(Region 居中裁剪)
    • 上屏:ArkUIImage直接消费PixelMap
  • 三、适配架构:语义层 / 引擎层 / 验收页三层
    • 三层各自职责与文件落点
    • 语义层只依赖自身接口,引擎层可替换、变换实现可插拔
  • 四、语义层:Painter / 状态机 / 变换契约(对照原库)
    • ImageData三源联合(Remote / Bytes / Resource)
    • ImagePainter绘制单元、ImageState状态机三接口
    • Transformation接口、LandscapistEngine平台接口、MemoryCache极简 LRU、ImageLoader.loadPainter门面
    • 与原库差异:pixelMap以Object形态跨层、ImageRequest.size建模成 ResizeTransformation
    • 分层与契约图
  • 五、引擎层: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 命名与类型的一点设计说明(本项目的适配决策)
    • 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)。
  • 上屏:ArkUIImage直接消费PixelMap(Image(pixelMap)),objectFit(ImageFit.Contain)控制缩放模式(ImageFit是 ArkUI全局枚举,不能从@kit.ArkUIimport)。

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

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

层文件职责是否碰@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),所以拆成三个具名接口再联合:

exportinterfaceRemoteData{readonlykind:'remote';readonlyurl:string;}exportinterfaceBytesData{readonlykind:'bytes';readonlydata:Uint8Array;}exportinterfaceResourceData{readonlykind:'resource';readonlyid:string;}exporttypeImageData=RemoteData|BytesData|ResourceData;

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

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


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

exportclassImagePainter{readonlypixelMap:Object|null;// 语义层只当 Object 持有readonlywidth:number;readonlyheight:number;constructor(pixelMap:Object|null,width:number,height:number){...}}

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

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

对应 LandscapistAsyncImagePainter的Loading/Success/Error:

exportinterfaceLoadingState{readonlystatus:'loading';}exportinterfaceSuccessState{readonlystatus:'success';readonlypainter:ImagePainter;readonlyfromCache:boolean;}exportinterfaceErrorState{readonlystatus:'error';readonlymessage:string;}exporttypeImageState=LoadingState|SuccessState|ErrorState;

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

4.4 变换接口Transformation(可插拔)

exportinterfaceTransformation{readonlykey:string;// 缓存键区分用transform(input:DecodedImage):Promise<DecodedImage>;// 返回(可原地改的)DecodedImage}

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

4.5 引擎接口 / 缓存 / 门面

exportinterfaceLandscapistEngine{fetch(url:string):Promise<Uint8Array>;decode(bytes:Uint8Array):Promise<DecodedImage>;}

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

asyncloadPainter(request:ImageRequest):Promise<ImageState>{constkey=cacheKeyOf(request);constcached=this.cache.get(key);if(cached!==undefined){return{status:'success',painter:newImagePainter(cached.pixelMap,cached.width,cached.height),fromCache:true};}try{letbytes:Uint8Array;if(request.data.kind==='remote')bytes=awaitthis.engine.fetch(request.data.url);elseif(request.data.kind==='bytes')bytes=request.data.data;elsereturn{status:'error',message:`resource 源暂未实现(id=${request.data.id})`};constdecoded=awaitthis.engine.decode(bytes);letcurrent=decoded;for(leti=0;i<request.transformations.length;i++){current=awaitrequest.transformations[i].transform(current);// 变换管线按序执行}this.cache.put(key,current);return{status:'success',painter:newImagePainter(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.1fetch—— 桥@ohos.net.http

asyncfetch(url:string):Promise<Uint8Array>{constrequest=http.createHttp();try{constoptions={method:http.RequestMethod.GET,expectDataType:http.HttpDataType.ARRAY_BUFFER};constresponse=awaitrequest.request(url,options);constresult=response.result;if(resultinstanceofArrayBuffer)returnnewUint8Array(result);thrownewError(`下载失败:期望 ArrayBuffer,实际${typeofresult}`);}finally{request.destroy();// 无论成败都释放,否则连接泄漏}}

5.2decode—— 桥@ohos.multimedia.image

asyncdecode(bytes:Uint8Array):Promise<DecodedImage>{constbuffer=bytes.buffer.slice(bytes.byteOffset,bytes.byteOffset+bytes.byteLength);// 取独立 ArrayBufferconstsource=image.createImageSource(buffer);constpixelMap=awaitsource.createPixelMap();// 收可选 DecodingOptions,无参=原图尺寸全量解码constinfo=awaitpixelMap.getImageInfo();constw=info.size.width;consth=info.size.height;// 尺寸在 info.size 上source.release();// 解码完即释放 ImageSource,避免句柄泄漏returnnewDecodedImage(pixelMap,bytes,w,h);}

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


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

exportclassResizeTransformationimplementsTransformation{asynctransform(input:DecodedImage):Promise<DecodedImage>{constpixelMap=input.pixelMapasimage.PixelMap;constinfo=awaitpixelMap.getImageInfo();constfx=this.width/info.size.width,fy=this.height/info.size.height;awaitpixelMap.scale(fx,fy);// 原地缩放constafter=awaitpixelMap.getImageInfo();input.width=after.size.width;input.height=after.size.height;returninput;}}exportclassCenterCropTransformationimplementsTransformation{asynctransform(input:DecodedImage):Promise<DecodedImage>{constpixelMap=input.pixelMapasimage.PixelMap;constinfo=awaitpixelMap.getImageInfo();constscale=Math.max(this.width/info.size.width,this.height/info.size.height);awaitpixelMap.scale(scale,scale);// 先放大覆盖constscaled=awaitpixelMap.getImageInfo();constx=Math.max(0,Math.floor((scaled.size.width-this.width)/2));consty=Math.max(0,Math.floor((scaled.size.height-this.height)/2));constregion={x,y,size:{width:this.width,height:this.height}};awaitpixelMap.crop(region);// 居中裁剪constfinalInfo=awaitpixelMap.getImageInfo();input.width=finalInfo.size.width;input.height=finalInfo.size.height;returninput;}}

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

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

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

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

if(this.state.status==='loading'){Column().width(160).height(160).borderRadius(12).backgroundColor('#EAEAEA')// 占位/骨架}elseif(this.state.status==='success'&&this.state.painter.pixelMap!==null){Image(this.state.painter.pixelMapasimage.PixelMap)// 成功:Painter 里的 PixelMap 上屏.width(160).height(160).objectFit(ImageFit.Contain)Text(`${this.state.painter.width}×${this.state.painter.height}·${this.state.fromCache?'命中内存缓存':'实时加载+解码+变换'}`)}elseif(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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询