Kotlin Multiplatform 三方库 Paging 3 的 OpenHarmony 鸿蒙化适配实战
Kotlin Multiplatform 三方库 Paging 3 的 OpenHarmony 鸿蒙化适配实战(androidx.paging 源码重建 + POSIX 互斥锁 + Compose LazyColumn 分页)
OpenHarmony 三方库社区地址:https://atomgit.com/oh-tpc
CPF-KMP-CMP 鸿蒙社区地址:https://atomgit.com/CPF-KMP-CMP
环境搭建指引:https://atomgit.com/CPF-KMP-CMP/docs
github 三方库地址:https://github.com/androidx/paging
官方文档地址:https://developer.android.com/kotlin/multiplatform/paging
鸿蒙定制仓库地址:https://maven.eazytec-cloud.com/nexus/repository/maven-public/
示例工程地址:https://atomgit.com/oh-tpc/KMP-Paging3-OHOS(本地工程 paging3-ohos/ 适配库 + cmp-paging3-demo/ Demo)
库版本:androidx.paging 3.4.0(自建 ohos 切片
3.4.0-ohos.1,上游最新为 3.5.3,见"关于版本基线")|验证环境:Kotlin Multiplatform 2.2.21-0.4.0(ovCompose 鸿蒙定制工具链,含 ohosArm64/ohosX64 target)|Compose Multiplatform 1.9.2-0.4.0|DevEco Studio 26.0.0|DevEco 模拟器 x86_64|HarmonyOS 7.0.0(API 26)
Paging 3 是 JetBrains / Google 官方的 Kotlin 分页库,Jetpack 体系里列表加载的事实标准。它的核心 paging-common 本身就是 Kotlin Multiplatform 库(PagingSource / Pager / PagingData 全是 common 代码),paging-compose 提供 collectAsLazyPagingItems() 把分页流接进 Compose 的 LazyColumn。官方只发 android/jvm/ios/native 变体,唯独没有 HarmonyOS。本文记录把 androidx.paging 源码克隆下来、补鸿蒙 target、本地重建成 klib 发 mavenLocal,再在 ovCompose(CMP)Demo 里用 Ktor MockEngine 造分页数据、跑通「分页请求 → PagingData → Compose 列表渲染 → 滚动翻页 → 失败重试」全链路的过程。
与之前适配的 SQLDelight、Settings 不同:Paging 3 是官方 KMP 库,鸿蒙化的难点不在"补平台代码",而在"让 androidx 的 KMP 工程结构在鸿蒙工具链下正确编出 ohos 变体"——真正要写的平台代码只有一份 POSIX 互斥锁,其余全是工程配置(sourceSet 层级、metadata 编译、依赖切片版本)。本文逐个排掉这些工程坑。

一、适配目标与整体链路
目标:在鸿蒙模拟器里跑一个 Compose 应用,列表用 Paging 3 驱动,滚动到底自动翻页,中途故意失败一次验证 LoadState.Error + 重试——证明 Paging 3 的分页管线(PagingSource → Pager → PagingData → LazyPagingItems)真的在鸿蒙 Kotlin/Native 上跑,而不是 UI 摆几条写死的数据。
整体链路(两段:先建库,再用库):
【适配库 paging3-ohos】
androidx/paging 源码 (paging-common + paging-compose)
│ 补 ohosArm64/ohosX64 target + ohosMain sourceSet
│ SynchronizedLock 用 pthread_mutex 补 actual
▼ publishToMavenLocal
~/.m2/repository/androidx/paging/.../3.4.0-ohos.1/ (ohosArm64 + ohosX64 klib + 根 .module)
【Demo cmp-paging3-demo】
commonMain: RepoPagingSource → RepoRepository(Pager) → Flow<PagingData<Repo>>
│ MockRepoApi (Ktor HttpClient + MockEngine,本地造分页数据)
▼ collectAsLazyPagingItems()
Compose LazyColumn (App.kt)
│ 编进 libkn.so
▼ NAPI
libentry.so → ArkTS Compose{} 组件 (Index.ets) → ArkUI 渲染
关键设计决策:不用华为仓现成的 paging 切片,而是克隆 androidx 官方源码本地重建。原因有二:① 征文要求"趟一遍三方库适配全流程",现成切片体现不出适配工作;② 只有自己重建,才能讲清楚 androidx KMP 工程在鸿蒙工具链下的真实坑点。数据层用 Ktor Client + MockEngine——真实走 Ktor 的请求/解析管线,但 MockEngine 拦截请求本地造数据,零外网依赖、可复现,且顺带证明 Ktor 在 OHOS 上跑通。
关于版本基线:本文以 paging-common 3.4.0 稳定线为基线验证整套鸿蒙化链路(上游最新已发布到 3.5.3)。选择 3.4.0 是因为它的 KMP 工程结构(nonJvmMain/nativeMain 源集布局)相对简单、坑面可控,适合先把链路跑通;本文总结的全部适配方法——ohos sourceSet 层级、POSIX 互斥锁 actual、legacy metadata 规避——作用在工程结构层而非业务代码层,与版本无关,可平移到 3.5.x 直接重建,切片版本号随基线改为 3.5.3-ohos.1 即可。
二、为什么 Paging 3 几乎"零平台代码"
先看 paging-common 的源码结构,理解适配面有多大:
paging-common/src/
commonMain/ PagingSource / Pager / PagingData / PagingConfig / LoadState ...(全 common)
nonJvmMain/ 原子类、Logger、PagingSourceFactory(atomicfu)
nativeMain/ SynchronizedLock 的 expect 壳
darwinMain/ SynchronizedLock actual(pthread,darwin 取值)
linuxX64Main/ SynchronizedLock actual(pthread,linux 取值)
...
PagingSource.load()、Pager.flow、PagingData 的 diff 全在 commonMain,纯 Kotlin + 协程,不碰任何平台 API。整个库里唯一的平台相关点是 SynchronizedLock(nativeMain 里是 expect,各平台给 actual),外加协程 Dispatchers。
所以鸿蒙适配的"代码量"极小:补一个 SynchronizedLock.ohos.kt(用 pthread_mutex),再把 sourceSet 层级和构建配置理顺。真正的工程量在后者。
三、适配过程:四个关键步骤
3.1 搭 ohos sourceSet 层级
paging-common/build.gradle.kts 里,鸿蒙 target 是 ohosArm64(真机)+ ohosX64(模拟器),但 androidx 的 applyDefaultHierarchyTemplate() 不含 ohos 组合,要显式补中间 sourceSet:
kotlin {
applyDefaultHierarchyTemplate()
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
freeCompilerArgs.add("-Xmulti-platform")
}
listOf(ohosArm64(), ohosX64()).forEach { target ->
target.compilations.getByName("main") {
cinterops.create("posix") {
defFile(file("src/ohosMain/cinterop/posix.def"))
compilerOpts("-I${file("src/ohosMain/cinterop")}")
}
}
}
sourceSets {
commonMain.dependencies {
api(libs.kotlinx.coroutines.core)
api(libs.androidx.annotation) // 见 3.4 坑1
}
// 复用原仓 nonJvmMain / nativeMain(原子类 / Logger / SynchronizedLock expect)
val nonJvmMain = sourceSets.maybeCreate("nonJvmMain")
nonJvmMain.dependsOn(sourceSets.getByName("commonMain"))
nonJvmMain.dependencies { implementation(libs.atomicfu) } // 只作库依赖,见坑2
val nativeMain = sourceSets.maybeCreate("nativeMain")
nativeMain.dependsOn(nonJvmMain)
// default hierarchy 不含 ohos,显式声明
val ohosMain = sourceSets.maybeCreate("ohosMain")
ohosMain.dependsOn(nativeMain)
sourceSets.getByName("ohosArm64Main").dependsOn(ohosMain)
sourceSets.getByName("ohosX64Main").dependsOn(ohosMain)
}
}
这样 ohosMain 挂在 nativeMain → nonJvmMain → commonMain 链上,自动继承所有 common 代码,SynchronizedLock 的 expect 也能被 ohos 的 actual 满足。
paging-compose 同理,只是中间层叫 nonAndroidMain(PlaceholderKey / uiDispatcher),依赖换成 project(":paging-common") + compose.runtime + compose.foundation。
3.2 POSIX 互斥锁补 SynchronizedLock actual
这是整个适配唯一要手写的平台实现。nativeMain 里 SynchronizedLock 是 expect,鸿蒙走 POSIX 线程模型,用 pthread_mutex(递归锁)补 actual:
// paging-common/src/ohosMain/kotlin/androidx/paging/internal/SynchronizedLock.ohos.kt
import platform.posix.* // 见坑3:包名必须是标准 platform.posix
@PublishedApi
internal class NativeMutex {
private val mutex = nativeHeap.alloc<pthread_mutex_t>()
init {
memScoped {
val attr = alloc<pthread_mutexattr_t>()
pthread_mutexattr_init(attr.ptr)
pthread_mutexattr_settype(attr.ptr, 0) // OpenHarmony musl: PTHREAD_MUTEX_RECURSIVE = 0
pthread_mutex_init(mutex.ptr, attr.ptr)
pthread_mutexattr_destroy(attr.ptr)
}
}
fun lock() = pthread_mutex_lock(mutex.ptr)
fun unlock() = pthread_mutex_unlock(mutex.ptr)
fun destroy() {
pthread_mutex_destroy(mutex.ptr)
nativeHeap.free(mutex.ptr.rawValue) // 见坑4:free 的是 rawValue
}
}
internal actual class SynchronizedLock actual constructor() {
@PublishedApi
internal val mutex = NativeMutex()
actual inline fun <T> withLockImpl(block: () -> T): T {
mutex.lock(); try { return block() } finally { mutex.unlock() }
}
@Suppress("unused")
private val cleaner = createCleaner(mutex, NativeMutex::destroy) // GC 时回收底层 mutex
}
三个细节都在注释里:
- 不依赖上游
SynchronizedLockNativeImpl/PTHREAD_MUTEX_RECURSIVE常量,直接自实现。因为 darwin 上该常量是Int、linuxX64 上是UInt(KT-41509),跨平台引用会翻车。OpenHarmony musl 里PTHREAD_MUTEX_RECURSIVE取值为0,直接写死。 createCleaner挂 GC 回收:nativeHeap.alloc的pthread_mutex_t不随 Kotlin 对象自动释放,用createCleaner在 GC 时回调destroy()。- 递归锁是必须的:Paging 内部有同线程重入路径(如
withLock里再触发withLock),非递归锁会死锁。
3.3 POSIX cinterop 配置
SynchronizedLock 要用 pthread_*,得先让 cinterop 生成 POSIX 绑定。posix.def:
headers = pthread.h
package = platform.posix
compilerOpts = -I src/ohosMain/cinterop
就两行有效配置,但 package 这一行是个坑(见 3.4 坑3)。
3.4 依赖切片版本与发布(坑最多的一步)
前三步把代码理顺了,真正折磨人的是构建。逐个排掉的坑:
坑1:androidx.annotation 无 OH 切片版本。
paging-common 依赖 androidx.annotation(@RequiresOptIn 等注解)。原仓锁的 1.9.x 没有 ohos 变体,换 1.10.0-0.1.0-02(eazytec 仓的 OH 切片)才行。
坑2:atomicfu 插件 id 不识别。
原仓用 atomicfu Gradle 插件做字节码变换,鸿蒙工具链下插件 id 解析失败。改成只把 atomicfu 当库依赖(implementation(libs.atomicfu)),不挂插件——Paging 用到的原子操作 atomicfu 库本身在鸿蒙上可用,不需要编译期变换。
坑3:cinterop package 不能自定义多级包名。
最初写 package = platform.posix.paging,cinterop 绑定生成失败。Kotlin/Native 的 POSIX 绑定必须用标准包名 platform.posix,不能自己造子包。
坑4:nativeHeap.free 的参数。
free(mutex) 编译错,inline 访问私有成员也有问题。正确姿势是 nativeHeap.free(mutex.ptr.rawValue)——free 要的是原生地址(rawValue),不是指针包装。
坑5(最隐蔽):@OptionalExpectation 与 legacy metadata 编译不兼容。
平台编译(compileKotlinOhosArm64 / compileKotlinOhosX64)早就成功了,但 publishToMavenLocal 卡在 compileCommonMainKotlinMetadata:annotation OH 切片里的 @RequiresOptIn 是 @OptionalExpectation 形式,legacy common metadata 编译器不认。
修复:禁用所有 legacy metadata 编译任务,但保留 kotlinMultiplatform publication(它产出根 .module,Gradle 靠它做变体解析):
tasks.matching {
it.name == "compileKotlinMetadata" ||
(it.name.startsWith("compile") && it.name.endsWith("KotlinMetadata"))
}.configureEach { enabled = false }
Demo 只消费 ohosArm64/ohosX64 变体(由平台编译生成,已验证可用),legacy metadata 本就用不上,禁掉无副作用。注意别误禁 kotlinMultiplatform publication——曾因此缺根 .module 导致变体解析失败,恢复该 publication、仅禁其编译任务即可。
坑6:jvm target 缺 lifecycle-viewmodel JVM 变体。
paging-compose 原仓带 jvm target,但它依赖的 lifecycle-viewmodel 没有对应 JVM 变体(且鸿蒙 Demo 根本用不到)。直接移除 jvm target 和未用的 viewmodel 依赖。
全部排完后:
cd paging3-ohos
$env:JAVA_HOME = "C:\Users\nwu\Desktop\z_pig\jdk-21.0.2"
.\gradlew.bat --no-daemon publishToMavenLocal
产物(双 ABI 齐全 + 根 .module):
~/.m2/repository/androidx/paging/
paging-common/3.4.0-ohos.1/ (.module 变体解析入口)
paging-common-ohosarm64/3.4.0-ohos.1/ (.klib)
paging-common-ohosx64/3.4.0-ohos.1/
paging-compose/3.4.0-ohos.1/
paging-compose-ohosarm64/3.4.0-ohos.1/
paging-compose-ohosx64/3.4.0-ohos.1/
四、Demo:Ktor MockEngine + Compose 分页列表
适配库建好,Demo 侧就是标准的 KMP 用法。cmp-paging3-demo/composeApp/build.gradle.kts 里双 target + 依赖 mavenLocal 切片 + Ktor:
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
// androidx.paging 鸿蒙切片(mavenLocal 3.4.0-ohos.1)
implementation("androidx.paging:paging-common:3.4.0-ohos.1")
implementation("androidx.paging:paging-compose:3.4.0-ohos.1")
// ktor client + MockEngine(本地造分页数据,OH 切片 3.3.3-1.1.0-02 双 ABI 齐全)
implementation("io.ktor:ktor-client-core:3.3.3-1.1.0-02")
implementation("io.ktor:ktor-client-mock:3.3.3-1.1.0-02")
implementation("io.ktor:ktor-client-content-negotiation:3.3.3-1.1.0-02")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.3.3-1.1.0-02")
}
4.1 PagingSource(核心验证点)
class RepoPagingSource(private val api: MockRepoApi, private val pageSize: Int = 20)
: PagingSource<Int, Repo>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Repo> {
val page = params.key ?: 1
return try {
val resp = api.fetchRepos(page, pageSize)
LoadResult.Page(
data = resp.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (resp.items.isEmpty() || page * pageSize >= resp.total) null else page + 1,
)
} catch (e: Exception) {
LoadResult.Error(e) // → LoadState.Error,UI 可 retry()
}
}
override fun getRefreshKey(state: PagingState<Int, Repo>) =
state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchor)?.nextKey?.minus(1)
}
}
纯 commonMain,无任何平台代码——这正是 Paging 3 KMP 适配的意义。
4.2 Ktor MockEngine 本地分页数据源
class MockRepoApi(private val total: Int = 87, private val networkDelayMs: Long = 600) {
private val client = HttpClient(MockEngine) {
install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
engine {
addHandler { request ->
val page = request.url.parameters["page"]?.toIntOrNull() ?: 1
delay(networkDelayMs) // 模拟网络往返,让加载态可见
// 第 3 页首次必失败一次,演示 LoadState.Error + 重试
if (page == 3 && FailOnce.shouldFail(page)) {
return@addHandler respond("""{"error":"..."}""", HttpStatusCode.InternalServerError, ...)
}
// 按 page 切片生成数据,序列化成 JSON 返回
respond(json.encodeToString(buildPage(page, ...)), HttpStatusCode.OK, ...)
}
}
}
suspend fun fetchRepos(page: Int, pageSize: Int): RepoPage {
val resp = client.get("https://mock.local/repos") { parameter("page", page); ... }
if (resp.status != HttpStatusCode.OK) throw RuntimeException("HTTP ${resp.status.value} on page $page")
return resp.body()
}
}
真实走 Ktor 引擎的请求 → ContentNegotiation → kotlinx.serialization 解析管线,只是 MockEngine 在传输层拦截、本地造 87 条数据分 5 页返回。这样零外网依赖、可复现,且证明了 Ktor client 在 OHOS 上完整跑通(请求构造、header、JSON 反序列化都是真的)。
4.3 Compose 消费 + Main 调度器
@Composable
fun App() {
val items = remember { RepoRepository().repoPager() }.collectAsLazyPagingItems()
LazyColumn {
items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
items[index]?.let { RepoCard(it) }
}
when (val append = items.loadState.append) {
is LoadState.Loading -> item { LoadingRow("加载更多…") }
is LoadState.Error -> item { ErrorRow("加载更多失败") { items.retry() } }
else -> {}
}
}
}
collectAsLazyPagingItems() 内部把 PagingData 的 diff 转成 Compose 快照,这一步依赖 Dispatchers.Main。鸿蒙上 Main 调度器不是默认有的,要在 MainArkUIViewController 里注册:
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: napi_env): napi_value {
initMainHandler(env) // ← 注册 Dispatchers.Main,否则 PagingData 收了 UI 不重组
return ComposeArkUIViewController(env) { App() }
}
少了 initMainHandler(env),列表会卡在首屏 loading——这是鸿蒙 CMP 特有的坑(StateFlow→UI 的 Main 调度要手动桥)。
五、运行效果(DevEco 模拟器实测)
以下截图均为模拟器实拍。模拟器是 HarmonyOS NEXT 7.0.0 / API 26(x86_64),跑的是按 API 26(compatibleSdkVersion: 26.0.0)编译的 HAP(命令行 hvigorw assembleHap 产出,94MB,双 ABI 各带 libkn.so),编译目标与模拟器版本一致。每一步的真实状态都用 uitest dumpLayout 抓控件树核实过(Repo #N 文本、错误文案、重试按钮 bounds),与截图一一对应。
首屏:顶部 Paging 3 × OpenHarmony,首屏 20 条卡片渲染(图中 #1–#10)。
滚动翻页:滚到 #26–#34,第 2 页(21–40)已 append 插入 → prefetch/append 机制在跑。
第 3 页失败:底部「加载更多失败:HTTP 500 on page 3 + 重试」按钮(MockEngine 故意失败一次)→ LoadState.Error 被正确捕获。
点重试后:可见 #36–#41,第 3 页成功(跨第 2 页末 40 → 第 3 页开头 41)→ retry() 生效。
末页终止:滚到 #82–#87(共 87 条,末页 7 条),无加载 footer → nextKey=null 生效。
验收流程(hdc 自动化可复现):
hdc install装 unsigned HAP →aa start -a EntryAbility -b ohos.cmp.paging拉起。- 首屏 20 条渲染。
uitest uiInput swipe 660 1900 660 200 400连续上滑 → 第 2 页 append。- 继续上滑 → 底部出现「HTTP 500 on page 3 + 重试」。
uitest dumpLayout取重试按钮 bounds[546,1932][775,2052],中心点click 660 1992→ 第 3 页加载成功。- 继续上滑到 #82–#87,无 footer → 末页终止。
整个过程中 PagingData 的增量 diff 只插入新条目,不整表刷新。
排障插曲:首次安装后 app 进程
HandleAppDied,faultlog 报ReferenceError: cannot find record '&../../oh_modules/@cpf-kmp-cmp/compose/Index&1.9.2-0.4.0'——@cpf-kmp-cmp/compose的.abc字节码没进 HAP。根因是命令行首次构建时oh_modules依赖的.abc未同步进modules.abc;ohpm install --all(在工程目录内跑,跨盘--prefix会报00618008 Cross Driver Error)+hvigorw clean重编后解决。这也是 FAQ Q8。
六、FAQ
Q1:Unresolved reference: paging?先在 paging3-ohos 里 publishToMavenLocal,且 Demo 的 dependencyResolutionManagement 里 mavenLocal() 要在仓库列表(放最前)。
Q2:发布 paging 时 metadata 编译报 @OptionalExpectation 相关错?annotation OH 切片的 @RequiresOptIn 是 optional-expectation 形式,legacy metadata 编译器不认。禁 compile*KotlinMetadata / generateMetadata* 任务,但保留 kotlinMultiplatform publication(产根 .module 做变体解析)。别误禁 publication,否则缺 .module 变体解析失败。
Q3:列表不刷新 / 卡在首屏 loading?鸿蒙上 Dispatchers.Main 不是默认有的,MainArkUIViewController 里必须 initMainHandler(env),否则 PagingData 的 diff 无法切到 Main 线程触发重组。
Q4:模拟器装不上,9568347 install parse native so failed?缺 x86_64 so。必须同时编 ohosX64,且 entry/build-profile.json5 的 abiFilters 含 "x86_64"。
Q5:ktor-client-mock 找不到 OH 变体?用 3.3.3-1.1.0-02(eazytec 仓,ohosArm64+ohosX64 双 ABI 齐全)。
Q6:Release 链接 OOM(DevirtualizationAnalysis)?sharedLib { if (buildType == RELEASE) optimized = false }。
Q7:cinterop package = platform.posix.paging 绑定失败?POSIX 绑定必须用标准包名 platform.posix,不能自定义子包。
Q8:free(mutex) 编译错?nativeHeap.free 要原生地址:nativeHeap.free(mutex.ptr.rawValue)。
Q9:app 装上就闪退,faultlog 报 ReferenceError: cannot find record '&../../oh_modules/@cpf-kmp-cmp/compose/Index&...'?@cpf-kmp-cmp/compose 的 .abc 没进 HAP。先在工程目录内 ohpm install --all(跨盘 --prefix 会报 00618008 Cross Driver Error),再 hvigorw clean 后重编。另外命令行编 HAP 要 hvigorw.bat 在 DevEco Studio/tools/hvigor/bin/ 下(不是 sdk/ 下),且需注入 node + jbr 到 PATH、DEVECO_SDK_HOME 指向 sdk。
Q10:命令行 uitest uiInput swipe 注入了但截图不变?Compose 渲染在 XComponent 上,snapshot_display 有帧延迟——截图可能拍到旧帧。判断真实滚动位置用 uitest dumpLayout 抓控件树里的 Repo #N 文本,比截图可靠。
七、总结与参考
把 Paging 3 跑上鸿蒙,和之前适配 SQLDelight、Settings 是两种完全不同的活:后两者难在"补平台驱动/桥接",Paging 3 是官方 KMP 库、平台代码只有一份 POSIX 互斥锁,难在让 androidx 的 KMP 工程结构在鸿蒙工具链下正确编出 ohos 变体——sourceSet 层级、@OptionalExpectation 与 legacy metadata 的兼容、依赖切片版本、cinterop 包名、nativeHeap.free 参数形态,每一个都是"看起来对但其实不对"的细节。
一旦 3.4.0-ohos.1 切片发进 mavenLocal,Demo 侧就是教科书式的 KMP:PagingSource / Pager / collectAsLazyPagingItems 全 common,业务零改动。这条链路打通,意味着鸿蒙 KMP 开发者可以直接用 Paging 3 处理任何长列表分页场景——这是 KMP 生产的刚需。
还没做的:接真实网络源(把 MockEngine 换成 ktor-client-curl 打真接口)、RemoteMediator(网络 + SQLDelight 本地双层缓存)、PagingData 的 map/cachedIn 操作符验证。但最难的部分——Paging 3 的 ohos klib 编出来、在鸿蒙上驱动 Compose 列表完成分页/重试/末页终止——已经打通,后面都是业务封装。
- 上游库:https://github.com/androidx/paging
- Paging 3 KMP 文档:https://developer.android.com/kotlin/multiplatform/paging
- 鸿蒙定制仓库:https://maven.eazytec-cloud.com/nexus/repository/maven-public/
- 鸿蒙切片:https://atomgit.com/oh-tpc/KMP-Paging3-OHOS
- OpenHarmony 三方库社区:https://atomgit.com/oh-tpc
- CPF-KMP-CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP
- CMP 鸿蒙开发环境搭建指南:https://atomgit.com/CPF-KMP-CMP/docs
更多推荐





所有评论(0)