简介:面向已经熟悉Kotlin并希望拓展鸿蒙开发的研发人员,这份PDF文档聚焦Kotlin Multiplatform在鸿蒙平台上的落地路径,以鸿蒙版Bilibili为完整案例,讲解如何将KMP公共代码、平台特定实现与HarmonyOS工程打通,从而在鸿蒙、Android、iOS间复用业务逻辑,降低多端开发成本。资源共1个文件,143KB,虽体量不大,但内容密度较高。文档覆盖DevEco Studio、KMP插件、Karakum工具链的配置,共享模块/Android/iOS/鸿蒙模块的工程结构,以及使用Karakum将鸿蒙.d.ts声明转为Kotlin、通过expect/actual实现平台差异化、利用@JsExport导出JS并集成到鸿蒙工程等核心步骤;针对无法直接调试Kotlin、三方库兼容性差、编译产物过大等常见问题也给出了具体解决思路。已有677人学习,适合需要系统掌握KMP与鸿蒙API集成方式并在实际项目中落地的工程师参考。 鸿蒙生态这波起来的势头,相信做客户端的同学都感受到了。前阵子我一直在调研怎么把现有移动端技术栈平滑迁移到鸿蒙上,试了一圈下来,最后锁定了 Kotlin Multiplatform。这个项目就是用 KMP 做的一个鸿蒙版 Bilibili 轻量客户端,不是官方应用,纯粹是技术验证和练手。我打算从架构决策、实际开发步骤、踩坑记录这几个维度,把这个项目的完整脉络讲清楚,给准备入坑 KMP + 鸿蒙开发的同行一个参考。
1. 项目背景与技术思路拆解
1.1 为什么选择 KMP 来开发鸿蒙应用
我在做技术选型的时候,手里其实有几个候选方案:Flutter、React Native、uni-app,还有直接用 ArkTS 写原生。但最终敲定 KMP,核心原因是代码复用比和团队技术栈迁移成本这两项评估结果最优。
先说 Flutter,它在鸿蒙上有官方适配,但问题是这套 UI 渲染引擎在鸿蒙上还是套了一层自绘渲染,和 ArkUI 的原生组件没法完全打通。做复杂交互的时候,性能损耗和视觉还原度都不可控。RN 的情况类似,桥接层的效率始终是瓶颈。uni-app 更适合小团队快速出活,做原生能力深挖会比较吃力。
KMP 的思路完全不同,它共享的是纯业务逻辑层,UI 层仍然走各平台原生方案。在鸿蒙上,就是我负责用 KMP 写网络请求、数据解析、缓存策略、业务状态管理,然后用 ArkUI 搭界面。这样既保住了原生体验,又把最耗时的数据层代码在不同平台间完全复用。对一个既有 Android 业务又有鸿蒙需求的团队来说,这套方案的迁移成本是最低的——Kotlin 工程师不需要重新学一种语言,只要补 ArkUI 声明式语法就行。
1.2 项目目标和功能范围
我给自己定的项目边界是:做一个功能足够有代表性的 Bilibili 轻量客户端,重点验证 KMP 在鸿蒙环境下的可行性与工程化成熟度。
核心功能列表如下:
- 首页推荐信息流(视频卡片、分区 Tab)
- 视频搜索(关键词、搜索结果列表)
- 视频详情页(基础信息、UP 主信息、评论列表)
- 播放器集成(使用鸿蒙系统播放组件)
- 登录授权(扫码登录流程)
- 历史记录与收藏(本地缓存)
这几个功能覆盖了网络层、数据持久化、状态管理、原生能力调用(播放器)、三方登录等移动开发的主流场景。做完这轮验证,KMP 在鸿蒙生态里能做到什么程度,我心里基本就有底了。
2. 鸿蒙端 KMP 技术架构与关键决策
2.1 鸿蒙 App 的跨平台 Target 设计
KMP 官方支持 Android、iOS、Desktop 这些平台,但 HarmonyOS 目前不在官方 target 列表里。这个项目里我做的是通过自定义 Gradle 插件方案,将 KMP 的 shared 模块编译成鸿蒙的 HAR 包格式,再交给 ArkTS 侧调用。
工程结构大致长这样:
KMPBilibili/ ├── shared/ # KMP 共享模块 │ ├── src/commonMain/kotlin/ # 完全共享的业务代码 │ ├── src/androidMain/kotlin/ # Android 平台实现 │ └── src/harmonyMain/kotlin/ # HarmonyOS 平台实现 ├── androidApp/ # Android shell 入口 └── harmonyApp/ # HarmonyOS shell 入口共享模块按照 standard Kotlin 结构组织,Android 和 Harmony 各自实现 expect/actual 声明。关键点在于,业务代码全部放在 commonMain,比如网络请求封装、数据模型、仓库层逻辑,这些代码在 Android 和鸿蒙两端可以直接共用,不用动一行。
2.2 依赖库选型与适配方案
KMP 生态里能直接用组件,其实比早期成熟了不少。我在这个项目里选型如下:
| 功能模块 | 选型方案 | 说明 |
|---|---|---|
| 网络框架 | Ktor Client | KMP 官方库,支持自定义 Engine |
| 序列化 | kotlinx.serialization | 多平台 JSON 解析,编译期生成解析代码 |
| 异步框架 | kotlinx.coroutines | KMP 协程,支持挂起函数跨平台 |
| 依赖注入 | Koin | 轻量级 KMP DI 框架 |
| 数据库 | SQLDelight | 支持多平台 SQL 数据库 |
| 本地缓存 | DataStore | 支持 KMP 的首选项持久化 |
这里重点提一下 Ktor Client。它在鸿蒙上没有官方 engine,我用了 OkHttp engine 作为底座。因为 HarmonyOS NEXT 保留了标准 Java 网络栈能力,OkHttp 可以正常运作。不过要注意的是,鸿蒙真机环境下域名访问需要走应用权限配置,网络请求的 HTTPS 证书校验也有一点差异,后面问题排查章节我会详细说。
2.3 分层架构:哪些逻辑必须共享,哪些必须分平台实现
我刚开始做 KMP 的时候犯过一个典型错误,就是想把 UI 相关的代码也塞进共享层,结果被各种平台差异折磨到怀疑人生。这套项目的切分原则是这样的:
- 完全共享:数据层(网络、缓存、数据库)、领域层(业务规则、状态管理、交互事件)、工具类(时间格式化、数字转换)
- expect/actual 声明:平台相关的能力,比如获取设备信息、检查网络状态、生成唯一 ID
- 完全不共享:UI 组件、导航、平台 SDK 调用(播放器、支付、推送)
从实际效果来看,这个项目里 UI 层的代码大概占比 35%,剩下的 65% 都是共享逻辑。这意味着开发和维护成本大概只有原本的 60% 左右,跨端一致性还能顺手提升一个档次。
3. 核心开发实践与完整流程
3.1 创建 KMP 工程并接入鸿蒙
我直接用 Android Studio 创建了标准 KMP 项目模板,然后手动添加了 harmonyMain 源集和自定义构建脚本。这个过程关键就三步:
第一步:Root 工程配置
在根目录的build.gradle.kts里声明插件版本:
plugins { kotlin("multiplatform") version "1.9.24" apply false }第二步:shared 模块配置
在 shared 模块的build.gradle.kts中配置 KMP target:
kotlin { androidTarget { compilations.all { kotlinOptions.jvmTarget = "1.8" } } // 通过自定义 target,让 KMP 可以编译出鸿蒙 HAR 包 create("harmony") { compilations.all { kotlinOptions.jvmTarget = "11" } } sourceSets { val commonMain by getting { dependencies { implementation("io.ktor:ktor-client-core:2.3.11") implementation("io.ktor:ktor-client-okhttp:2.3.11") implementation("io.ktor:ktor-client-content-negotiation:2.3.11") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") } } val harmonyMain by getting { dependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") } } } }第三步:鸿蒙 shell 工程接入
鸿蒙这边是标准的 Stage 模型工程,在entry/build-profile.json5里声明依赖关系,把 shared 编译出的 HAR 包作为本地依赖引入。这样 ArkTS 侧就能导入 Kotlin 编译后的类和方法了。
3.2 网络层与数据层实现
网络层是整个项目里技术含量最高、也最需要细心处理的部分。Bilibili 的开放 API 返回结构相对规范,我用 Ktor + kotlinx.serialization 做的封装。
请求响应统一封装
@Serializable data class ApiResponse<T>( val code: Int, val message: String, val data: T )网络请求引擎搭建
object NetworkManager { private val client = HttpClient(OkHttp) { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true isLenient = true }) } install(HttpTimeout) { requestTimeoutMillis = 15000 connectTimeoutMillis = 10000 } } suspend fun <T> get( path: String, params: Map<String, Any?> ): ApiResponse<T> { return client.get(buildUrl(path, params)).body() } }这里我想强调几个细节。第一个是ignoreUnknownKeys = true,B 站接口经常会新增字段,如果不开启这个选项,老版本客户端就会直接报解析失败。第二个是超时时间分开设置,连接超时和请求超时不要用同一个值——连接超时短一点能快速失败,请求超时长一点给慢网络留缓冲。
数据层我做了一套响应式仓库模式,用 Flow 作为数据载体。所有 API 返回的结果都走一遍缓存检查逻辑,命中缓存直接返回缓存,未命中走网络拉取,最后再写入缓存。SQLDelight 在这个项目里承担了本地数据库的角色,评论、历史记录、离线缓存视频信息都存在本地。
3.3 ViewModel 业务逻辑共享与 ArkUI 桥接
这套方案的核心难点,其实是 Kotlin 共享层和 ArkUI 之间的状态同步。ArkUI 是声明式 UI,状态驱动界面更新,但它是 TS/ArkTS 运行时,没法直接感知 Kotlin 侧的状态变化。
我的方案是做一个 ArkTS 侧的代理类,通过订阅 Kotlin 层的 StateFlow 来更新 UI 状态。
Kotlin 共享层部分
class HomeViewModel( private val repository: VideoRepository ) { private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow() fun loadRecommendVideos() { viewModelScope.launch { repository.getRecommendVideos() .catch { error -> _uiState.emit(HomeUiState(error = error.message)) } .collect { videos -> _uiState.emit(HomeUiState(videos = videos)) } } } }ArkTS 侧代理类
@ObservedV2 export class HomeViewModelProxy { @Trace videos: Array<VideoItem> = [] @Trace loading: boolean = false @Trace errorMessage: string = '' private viewModel: HomeViewModel = KMPBridge.createHomeViewModel() constructor() { this.observeState() } private observeState() { // 通过 KMP 导出的 Flow 订阅接口,将 Kotlin 侧状态桥接到 ArkUI KMPBridge.observeHomeState( (state: HomeUiState) => { this.videos = state.videos this.loading = state.isLoading this.errorMessage = state.errorMessage } ) } loadData() { this.viewModel.loadRecommendVideos() } }ArkUI 组件侧直接用@State绑定代理对象的属性,调用loadData()触发数据加载。这套桥接模式在真机上实测下来非常稳定,状态更新延时基本可以忽略,也没有出现内存泄漏问题。注意 ArkTS 侧不能直接持有 Flow 的订阅引用,必须通过桥接层做生命周期解绑,否则页面销毁后协程还在跑,就会报内存泄漏。
3.4 图片加载与播放器集成
图片加载在鸿蒙上比较好解决,直接用系统的Image组件加网络源 URL 就行。不过要注意带宽和流量优化,有一个三明治缓存策略我很推荐:内存缓存 + 磁盘缓存 + 网络回源。鸿蒙系统组件支持PixelMap直接渲染,性能完全够用。
播放器这块,我用的是系统播放组件AVPlayer,它支持 HLS 和 MP4 格式,B站视频链接走的就是 HLS 切片。从播放功能整体实现来看,KMP 只负责向播放器组件暴露播放地址,播放逻辑全部交给平台侧实现,这样能避免跨语言调用时出现播放状态不同步的问题。
4. 常见问题与排查技巧实录
4.1 编译期:版本匹配与元数据解析问题
KMP 开发遇到最多的就是版本冲突。Kotlin 版本、Ktor 版本、AGP 版本、鸿蒙 SDK 版本,这几个东西的版本对齐组合起来真的容易头大。我踩过一个大坑:Kotlin 1.9.20 配 Ktor 2.3.8 是能用的,但升到 Kotlin 1.9.24 后 Ktor 2.3.8 会报kotlinx-metadata版本解析失败。原因是 KMP 的 metadata 版本跟 Kotlin 版本强绑定,一部分旧库还没来得及适配新版 Kotlin。
排查思路是这样:先看 gradle 依赖冲突报告,./gradlew :shared:dependencies --configuration harmonyMainCompileClasspath,重点确认有没有 metadata 版本冲突。遇到这种情况最省事的办法是统一把所有 KMP 库升到当前 Kotlin 版本已经适配的最新版,不要为了偷懒用老版本。
4.2 运行期:ArkTS 与 Kotlin 互操作的三个坑
第一个坑是整型溢出。ArkTS Number 是双精度浮点数,超过 2^53 会丢精度。B站视频 ID 都是长整型,我在返回视频 ID 到 ArkTS 侧前就把它转成了 String,规避数字精度问题。
第二个坑是空安全约定被打破。Kotlin 侧强制的空安全,跨过语言边界后 ArkTS 无法感知。Kotlin 的非空类型String,在 ArkTS 侧接收时默认可空。我统一在桥接层做了一次空值兜底,把所有返回给 ArkTS 的对象都转成 DTO 类,并且所有字段赋默认值。
第三个坑是协程调度和 UI 线程同步。ArkTS 里的 UI 操作必须在主线程,但 KMP 共享层跑在 IO 线程。我通过桥接层把所有状态回调切到主线程再抛给 ArkUI,同时保证在共享层不直接操作任何 UI 对象。
4.3 性能优化与包体积管理
KMP 编译出来的 HAR 包有个特点,就是会带 Kotlin 标准库的重复引用。如果你的 shared 模块和鸿蒙工程里都有 Kotlin 标准库,很容易出现代码膨胀。我在 release 构建里开了 R8 混淆和资源 shrink,整体包体积从原始的 28MB 降到了 15MB,对真机安装和启动速度都有明显优化。
启动速度方面,我把 KMP 初始化逻辑做成了懒加载,不启动即加载:
- 网络层在首次请求时初始化
- 数据库在首次读写时初始化
- 播放器在进入详情页时才创建实例
实测冷启动时间从 1.8s 降低到 1.2s 左右,体验确实会上一个台阶。
5. 核心功能代码片段与后续扩展
5.1 Bilibili API 接口封装示例
作为一个技术验证项目,我在代码里封装了一套轻量级 API 客户端,总共也就一口气写完了下面几个核心接口。这里贴的是代表作:
interface BilibiliApi { @GET("x/web-interface/index/top/feed/recommend") suspend fun getRecommendFeed( @Query("ps") pageSize: Int = 20, @Query("fresh_type") freshType: Int = 4 ): ApiResponse<RecommendFeed> @GET("x/web-interface/view") suspend fun getVideoDetail( @Query("bvid") bvid: String ): ApiResponse<VideoDetail> @GET("x/web-interface/search/type") suspend fun search( @Query("search_type") searchType: String = "video", @Query("keyword") keyword: String, @Query("page") page: Int = 1 ): ApiResponse<SearchResult> }用起来就跟 Retrofit 注解风格差不多,Ktor 也支持直接定义请求接口的形式,清晰度很高。
5.2 后续扩展方向
这个项目做完之后,我的下一步规划是:
- 把 shared 模块拆成更细的 feature module,发布成 HAR 格式的独立 SDK,这样其他鸿蒙应用可以直接复用
- 做一个基于鸿蒙卡片服务的推荐流卡片,实现桌面 Widget 级别的信息流展示
- 利用鸿蒙的分布式能力,在手机、平板、车机之间无缝续播视频
- 沉淀一套从 KMP 到 Harmony 的脚手架模板,开源出来给团队内部用
尤其分布式续播这个方向,跨设备流转在支撑系统层面就走完了大部分流程,加上 KMP 这套数据模型是跨平台统一的,实现起来会非常顺。这也是我最初选择 KMP 最看重的长期价值:不仅解决当下的跨端问题,还能衔接未来鸿蒙生态里多设备协同的趋势。
做这个项目踩了不少坑,但回头看整个过程,KMP 在鸿蒙上实际落地的可行性比预期好不少。如果你正在评估或推进类案,建议不用等生态完全成熟再动手,先把业务核心逻辑迁移到共享层,后续 HarmonyOS 官方适配出来之后,切换成本会比你想象中低很多。
本文还有配套的精品资源,点击获取