Kotlin Multiplatform移植实战:共享业务逻辑的跨平台工程方案 这次我们来看一个非常典型的跨平台工程问题星球突击队 Kotlin Multiplatform 移植。直接把话说在前头这个项目不是指某一个特定的开源仓库而是“把一套 Kotlin 代码通过 KMP 架构移植到 Android、iOS、桌面端等多平台”的完整工程方案。无论你是从原生 Android 项目往 iOS 扩展还是想把一套游戏逻辑、业务模块同时跑在多个端上Kotlin Multiplatform简称 KMP现在都是值得优先考虑的技术路线。KMP 的核心卖点不是“一套代码走天下”这种夸张口号而是共享业务逻辑、保留原生体验、按需复用 UI。它最务实的使用方式是把网络层、数据存储、状态管理、工具类这些和平台无关的逻辑全部抽到共享模块UI 层仍然用原生方案去写或者用 Compose Multiplatform 做跨平台 UI。本文从应用层出发讲清楚 KMP 移植的完整路径环境准备 - 工程评估 - 模块划分 - 依赖配置 - 代码改造 - 功能测试 - 接口设计 - 常见问题排查。适合正在做 Android/iOS 双端项目的团队也适合打算把已有 Kotlin 工程往多平台方向迁的开发者。1. 核心能力速览能力项说明项目类型Kotlin Multiplatform 跨平台应用移植目标平台Android、iOS、桌面端JVM/Windows/macOS/Linux核心技术Kotlin Multiplatform、expect/actual 机制、Gradle 多模块工程共享范围业务逻辑、数据层、网络层、状态管理、工具库UI 方案原生 UI 或 Compose Multiplatform核心难点平台差异隔离、依赖替换、多平台依赖树管理、打包产物配置入门门槛需要熟悉 Kotlin 和 Gradle理解各平台构建体系适用项目中大型客户端项目、游戏逻辑层、工具类 SDK、跨端业务组件从工程实践看KMP 移植最值钱的地方是帮团队把“三端三份逻辑”压缩成“一份共享逻辑 三份轻量适配层”。代价是前期需要投入学习成本和构建链路调试时间。2. 适用场景与使用边界2.1 适合什么人KMP 移植特别适合下面几类场景已有 Android 原生工程想低成本扩展到 iOS 端。团队熟悉 Kotlin不想引入 Flutter/React Native 那套全新的 UI 生态。需要共享网络请求、本地存储、账号体系、埋点上报、加密解密等底层逻辑。在做游戏或应用的基础组件层希望一套逻辑同时跑在 Android、iOS、桌面端。打算用 Compose Multiplatform 把 UI 层也统一起来但希望按模块渐进式迁移。2.2 能解决什么问题最直观的收益是减少重复开发。比如登录流程、数据解析、消息推送、数据库访问这些逻辑原来 Android 写一遍、iOS 用 Swift 再写一遍逻辑稍微不一致就会出现两端行为不同的问题。KMP 共享之后算法逻辑、状态机、校验规则这些只有一份。另一个收益是测试成本下降。共享模块可以直接在 JVM 上跑单元测试不需要每次都启动模拟器。这对纯 Kotlin 逻辑的覆盖效率提升非常明显。2.3 不适合什么情况对 UI 高度定制、大量依赖系统控件的应用KMP 共享 UI 层的性价比会下降。团队没有 Kotlin 经验一上来就搞 KMP学习成本会叠加在业务开发上。核心性能瓶颈在平台特有 API 上的项目比如大量依赖 CoreML、ARKit、Camera2 的项目共享逻辑占比小移植收益有限。处于快速原型期的项目KMP 的工程复杂度不值得提前引入。2.4 版权、隐私与合规边界做 KMP 移植时要特别注意移植第三方 SDK 或开源库时确认许可证是否允许跨平台重新编译和分发。涉及用户数据的共享模块两个平台都要走各自的数据保护合规流程不能因为逻辑共享就忽略平台隐私政策。游戏或应用的素材、美术资源、音频文件上传到共享模块后要确认授权范围是否覆盖所有目标平台。不要把平台限制的接口通过 expect/actual 技术绕过审核所有移植都要遵守目标平台的应用市场规则。3. 环境准备与前置条件3.1 开发工具链工具用途说明JDK编译 Kotlin/JVM 代码推荐使用 JDK 11 或更高版本Android StudioAndroid 端开发与构建需要安装 Kotlin Multiplatform 插件XcodeiOS 端编译与调试仅 macOS 环境需要Kotlin 插件Gradle 构建支持版本以官方最新稳定版为准Android SDKAndroid 平台 API通过 SDK Manager 安装CocoaPodsiOS 端依赖管理可选取决于是否使用 Pod 集成3.2 硬件要求KMP 本身对硬件没有特殊硬性要求普通开发机能跑 Android Studio 就够了。但如果要同时编译 iOS 端必须使用 macOS。Windows 和 Linux 上可以做共享逻辑的开发但 iOS target 只能交给 macOS 构建机。如果是做 CI 持续集成建议准备 macOS 的构建机器配置至少 16GB 内存和 100GB 以上磁盘空间。Android 和 iOS 的构建产物加起来体积不小。3.3 开发环境的检查项动手之前先确认下面这些项java -version # java version 17.0.x 比较好低版本可能和最新 Gradle 插件不兼容 gradle -v # 或者看项目里的 gradle wrapper 版本 adb version # Android 调试桥正常如果是 macOS 上做 iOS 编译还需要确认xcodebuild -version pod --version3.4 网络与依赖仓库KMP 工程会用到 Maven Central 和 Google 的 Maven 仓库国内网络环境下建议配置镜像加速。在~/.gradle/init.gradle或者项目settings.gradle.kts中配置仓库时优先选择可达的镜像地址。4. 移植前的工程评估与架构设计移植不是把代码文件直接复制到共享模块就完事最忌讳上来就改 Gradle 配置。先做评估和架构设计后面会省很多事。4.1 盘点现有代码按下面几个维度把现有代码过一遍代码类别示例移植策略纯逻辑代码数学计算、字符串处理、校验规则直接放入共享模块平台无关的数据模型DTO、实体类、枚举直接放入共享模块依赖 Android API 的代码Context、SharedPreferences、Toast包一层接口用 expect/actual 适配依赖 iOS API 的代码UserDefaults、Keychain、NSURLSession包一层接口用 expect/actual 适配第三方 SDKFirebase、友盟、微信登录保留在各端通过接口抽象隔离这一步的目标是把工程分成三层共享逻辑层、平台适配层、平台应用层。4.2 确定共享边界不是所有代码都值得共享。我的建议是优先共享网络请求和响应解析数据持久化数据库、Key-Value 存储登录态管理和 Token 刷新业务状态机和规则引擎埋点数据组装日志输出通用工具类日期、加密、文件路径处理保留在平台层的代码复杂 UI 组件和页面导航系统能力调用相机、定位、传感器推送注册和通知处理支付和账号授权平台特有的动画和渲染4.3 设计模块结构建议按 Feature 和 Core 拆模块而不是把所有共享逻辑都塞进一个大shared模块。模块拆太粗会导致编译增量变慢、多人协作冲突拆太细又会增加维护成本。推荐结构project-root/ ├── shared/ │ ├── core/ // 基础工具、网络、存储抽象 │ ├── auth/ // 登录注册模块 │ ├── profile/ // 用户信息模块 │ └── game/ // 星球突击队核心玩法逻辑 ├── androidApp/ ├── iosApp/ └── build.gradle.kts4.4 明确 UI 处理方案如果当前项目已经有完整原生 UI第一版 KMP 移植不要碰 UI。先把逻辑层共享UI 继续用原生写等逻辑稳定了再评估是否引入 Compose Multiplatform。如果你的团队是从零开始项目没有历史包袱可以考虑直接用 Compose Multiplatform 做一套 UI 覆盖 Android、iOS、桌面端。但要对兼容状态和性能表现做充分测试尤其是 iOS 端。5. 项目改造与依赖配置这一节是实际操作核心。所有配置代码都按常规 Kotlin Multiplatform 工程结构给出具体包名、模块名要按你的实际项目替换。5.1 根目录 settings.gradle.ktspluginManagement { repositories { mavenCentral() google() gradlePluginPortal() } } dependencyResolutionManagement { repositories { mavenCentral() google() } } rootProject.name PlanetStriker include(:shared:core) include(:shared:auth) include(:shared:profile) include(:shared:game) include(:androidApp)5.2 根目录 build.gradle.ktsplugins { kotlin(multiplatform) version 2.0.0 apply false kotlin(android) version 2.0.0 apply false kotlin(plugin.serialization) version 2.0.0 apply false id(com.android.application) version 8.5.0 apply false id(com.android.library) version 8.5.0 apply false }注意版本号要和你本地的 Android Studio、JDK 版本匹配。写死版本不是最优选择建议用较新的稳定版本。5.3 共享模块 shared/build.gradle.kts这是一个典型的多平台模块配置plugins { kotlin(multiplatform) kotlin(plugin.serialization) id(com.android.library) } kotlin { androidTarget { compilations.all { kotlinOptions { jvmTarget 17 } } } listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { iosTarget - iosTarget.binaries.framework { baseName Shared isStatic true } } sourceSets { val commonMain by getting { dependencies { implementation(org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1) implementation(org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1) implementation(io.ktor:ktor-client-core:2.3.12) implementation(io.ktor:ktor-client-content-negotiation:2.3.12) implementation(io.ktor:ktor-serialization-kotlinx-json:2.3.12) } } val commonTest by getting { dependencies { implementation(kotlin(test)) } } val androidMain by getting { dependencies { implementation(io.ktor:ktor-client-okhttp:2.3.12) implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5) } } val iosMain by getting { dependencies { implementation(io.ktor:ktor-client-darwin:2.3.12) } } } } android { namespace com.example.planetstriker.shared compileSdk 35 defaultConfig { minSdk 24 } }5.4 Android 宿主应用配置Android 应用模块直接依赖共享模块// androidApp/build.gradle.kts dependencies { implementation(project(:shared:game)) implementation(project(:shared:auth)) // 其他 Android 依赖 }5.5 iOS 端集成方式iOS 端有两种集成方案方案一直接生成 Framework拖入 Xcode 工程。方案二通过 CocoaPods 集成。在shared模块中启用 CocoaPodskotlin { cocoapods { summary Shared module for PlanetStriker homepage https://example.com version 1.0.0 framework { baseName Shared } ios.deploymentTarget 12.0 } }然后在 iOS 工程的Podfile中添加target iosApp do use_frameworks! pod Shared, :path ../shared end构建命令cd shared ./gradlew podspec pod install --project-directory../iosApp6. 功能测试与效果验证KMP 移植完成后不能只验证能不能编译通过要建立一套完整的验证流程。6.1 共享逻辑单元测试共享模块的代码可以直接在 JVM 上跑测试这是 KMP 比较舒服的一点。在shared/core/src/commonTest/kotlin/下新建测试文件import kotlin.test.Test import kotlin.test.assertEquals class TokenValidatorTest { Test fun testTokenExpired() { val token AuthToken( value abc123, expiresAt 1000L, now { 2000L } ) assertEquals(true, token.isExpired()) } }运行测试./gradlew :shared:core:testDebugUnitTest6.2 Android 端功能验证在 Android 模拟器或真机上验证下面的核心链路验证项操作步骤预期结果登录流程输入账号密码点击登录请求发出、Token 写入本地存储、登录状态更新数据缓存断网后重新进入页面共享模块缓存数据可读取玩法逻辑执行一局游戏或任务状态机流转正确比分计算正确网络切换从 WiFi 切到 4G/5G请求不崩溃能自动重试或提示6.3 iOS 端功能验证同一条业务链路在 iOS 端也要完整跑一遍重点检查共享 Framework 是否被正确链接。网络库 Ktor Darwin 引擎是否正常发请求。UserDefaults 通过 expect/actual 封装后读写是否正常。同一个 Token 在 Android 和 iOS 端互相验证是否通过。6.4 双端一致性验证这是最关键的验证同一个操作在两个端上的业务结果必须一致。具体做法是准备一份测试用例清单包含登录、登出、数据刷新、异常处理等场景在两端执行并对比结果。可以做成离线的数据对比测试// 生成测试数据 val input TestDataFactory.createBattleRecord() val androidResult processBattleRecord(input) // iOS 端导出相同数据跑同一种处理逻辑后对比输出一致才说明共享逻辑链路的移植没有偏差。7. 接口设计与数据层移植7.1 用 expect/actual 封装平台差异KMP 不可能把所有平台 API 都抹平遇到平台差异要靠expect/actual做适配。在公共模块里声明一个通用的 Token 存储接口// shared/core/src/commonMain/kotlin/com/example/core/storage/TokenStorage.kt expect class TokenStorage { fun saveToken(token: String) fun getToken(): String? fun clearToken() }Android 端实现// shared/core/src/androidMain/kotlin/com/example/core/storage/TokenStorage.android.kt import android.content.Context actual class TokenStorage(private val context: Context) { private val prefs context.getSharedPreferences(app_prefs, Context.MODE_PRIVATE) actual fun saveToken(token: String) { prefs.edit().putString(auth_token, token).apply() } actual fun getToken(): String? prefs.getString(auth_token, null) actual fun clearToken() { prefs.edit().remove(auth_token).apply() } }iOS 端实现// shared/core/src/iosMain/kotlin/com/example/core/storage/TokenStorage.ios.kt import platform.Foundation.NSUserDefaults actual class TokenStorage { private val defaults NSUserDefaults.standardUserDefaults actual fun saveToken(token: String) { defaults.setObject(token, forKey auth_token) } actual fun getToken(): String? defaults.stringForKey(auth_token) actual fun clearToken() { defaults.removeObjectForKey(auth_token) } }这种方式把平台差异收敛到最薄的一层业务代码不需要知道底层是 SharedPreferences 还是 NSUserDefaults。7.2 网络层设计网络层推荐用 Ktor Client天然支持多平台。核心思路是在 commonMain 中定义 API 接口和 DTO各平台只需要选择适合自己的引擎。// shared/core/src/commonMain/kotlin/com/example/core/network/ApiClient.kt import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json class ApiClient(private val baseUrl: String) { private val client HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys true isLenient true }) } } suspend fun login(username: String, password: String): LoginResponse { return client.post($baseUrl/api/login) { setBody(LoginRequest(username, password)) }.body() } suspend fun fetchBattleData(battleId: String): BattleData { return client.get($baseUrl/api/battle/$battleId).body() } }Android 端使用 OkHttp 引擎iOS 端使用 Darwin 引擎代码逻辑完全一致只有 Gradle 依赖不同。7.3 数据库与本地存储如果业务需要本地数据库建议使用 SQLDelight 或 Room 的 KMP 支持。SQLDelight 的优势是 SQL 语句在所有平台统一执行生成类型安全的查询接口。-- shared/core/src/commonMain/sqldelight/com/example/core/db/Player.sq CREATE TABLE player ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, level INTEGER NOT NULL, score INTEGER NOT NULL ); selectAll: SELECT * FROM player; insertPlayer: INSERT OR REPLACE INTO player(id, name, level, score) VALUES (?, ?, ?, ?); updateScore: UPDATE player SET score ? WHERE id ?;注意 SQLDelight 的版本和 Kotlin 版本要匹配否则会出现编译期错误。7.4 数据模型与序列化用 kotlinx.serialization 定义跨平台数据模型// shared/game/src/commonMain/kotlin/com/example/game/model/BattleRecord.kt Serializable data class BattleRecord( val battleId: String, val playerId: String, val enemyType: String, val score: Int, val durationSeconds: Int, val victory: Boolean )数据模型的字段命名要保持稳定避免双端各自反序列化时出现不一致。8. 常见问题与排查方法KMP 移植中遇到的坑大多数集中在依赖配置、链接错误、构建版本不一致和本地存储差异上。整理一份排查清单问题现象可能原因排查方式解决方案同步 Gradle 报错仓库源不可达或插件版本不匹配查看gradle.log检查仓库镜像配置国内镜像升级或降低插件版本iOS Framework 生成失败Xcode 版本与 Kotlin 版本不兼容运行./gradlew :shared:linkDebugFrameworkIosSimulatorArm64查看详细日志检查 Kotlin 版本和 Xcode 版本匹配表Kotlin 编译时找不到 expect 的 actual 实现actual 声明放错了 source set检查androidMain、iosMain目录结构把 actual 实现放到对应平台 source setAndroid 端 NoClassDefFoundError共享模块没有被应用模块依赖查看androidApp/build.gradle.kts依赖配置添加 project 依赖iOS 端 Undefined symbolsFramework 未正确链接查看 Xcode 的 Link Binary With Libraries手动添加 Shared.framework数据库文件路径不一致各平台默认数据库目录不同打印实际路径在 expect/actual 中显式指定数据库目录网络请求在 iOS 上不发送缺少 NSAppTransportSecurity 配置查看 Xcode Info.plist添加 ATS 例外或改用 HTTPS序列化报错JSON 字段和 Kotlin 属性不一致打印原始响应在 DTO 上使用SerialName对齐字段名协程调度异常在 Main 线程做耗时操作查看日志中的 Dispatchers 使用正确切换到 Dispatchers.IOURLSession 请求慢多线程并发导致查看网络时序使用 HttpClient 的线程池配置优化8.1 依赖安装失败KMP 工程涉及 Gradle 插件、Kotlin 插件、Android Gradle Plugin、原生工具链等多层依赖最容易踩坑的是版本兼容。处理思路是以官方最新稳定 KMP 版本为基准反查 Android Gradle Plugin 和 Kotlin 版本的兼容说明。不要所有依赖都用 latest也不要混用大版本。8.2 模型文件或资源文件缺失共享模块中如果引入了资源文件需要检查对应 source set 的目录是否正确。放在commonMain/resources下只能用于逻辑初始化真正的平台资源图片、音频仍然建议放在各端自己的资源目录。8.3 显存或内存不足如果移植的是游戏相关逻辑大量场景对象驻留内存时需要在共享模块里做好对象生命周期管理及时释放资源引用。各端的虚拟机内存策略不同不用平台统一标准去衡量要以两端独立测试为准。8.4 端口冲突或本地服务访问失败开发阶段如果共享模块内嵌了本地文件服务或调试端口注意两个平台端口不能冲突。统一把端口配置放到公共常量里。8.5 构建产物不稳定如果出现“改了代码但运行没有变化”的问题多半是增量编译缓存没有命中。清理构建缓存./gradlew clean rm -rf .gradle rm -rf buildmacOS 上还可以清理 Xcode 的 DerivedData 再去重新编译。9. 最佳实践与使用建议9.1 分阶段迁移不要尝试“一夜之间全部迁到 KMP”。建议按下面的节奏走把无依赖的纯工具类迁到共享模块。引入网络层和数据模型。实现登录、数据缓存等核心链路。再逐步收敛业务逻辑。最后评估 UI 层是否引入 Compose Multiplatform。每一阶段都要设置“可回退点”确认没有严重问题再进入下一阶段。9.2 模块拆分宁细勿粗虽然模块多了会增加配置量但从长期维护看按功能域拆分共享模块会让编译增量更清晰也让每个人改动的范围更可控。建议至少拆出core网络、存储、日志、通用工具auth登录注册user用户信息battle战斗玩法逻辑rank排行榜9.3 保留最小可运行配置建议维护一个“最小 KMP 模板工程”包含最基础的 Android iOS 双端构建链路。每次升级 Kotlin 版本、Gradle 版本或 Ktor 版本前先在这个模板里验证再对正式项目操作。9.4 批量化处理编译任务如果共享模块多每次验证都要全量编译很耗时。可以只编译改动的模块./gradlew :shared:core:compileKotlinAndroid ./gradlew :shared:core:linkDebugFrameworkIosSimulatorArm64CI 中也可以拆成多个 job分别编译 Android 和 iOS。9.5 接口设计规范共享模块的接口要尽量保持平台无关不要直接暴露 expect 类。对外统一暴露接口加数据模型内部实现细节不暴露到业务层。9.6 日志与状态管理KMP 移植后两端日志格式要尽量统一。可以自己封装一个基于端口的日志模块expect fun logDebug(tag: String, message: String) actual fun logDebug(tag: String, message: String) { // Android 使用 Log.d } actual fun logDebug(tag: String, message: String) { // iOS 使用 NSLog }这样排查问题时两端日志可以放到同一个聚合系统中分析。9.7 合规使用提醒共享模块可能被两个平台同时使用任何涉及第三方代码的引入都要做许可证检查。如果项目要商用优先使用 Apache 2.0、MIT 等宽松许可证的库。涉及用户数据、定位信息、通讯录等敏感权限时两端都要遵循平台隐私规范不能因为共享逻辑就绕过系统授权。10. 总结与下一步Kotlin Multiplatform 移植解决的核心问题是“一份业务逻辑多端保持一致”。从实践角度看最值得先做的并不是把整个项目翻成 KMP而是先把网络层、数据层、状态管理这类和 UI 无关的代码抽取出来放到共享模块中跑通双端构建链路。最容易踩的坑有三个一是 Gradle 多模块依赖配置不兼容二是 expect/actual 声明放错 source set三是 iOS 端 Framework 链接失败。这三个问题都会在构建阶段暴露解决思路分别是先跑通最小模板、严格按目录放置平台代码、检查 Xcode 的链接配置。下一步要做的验证也很明确先跑起来最小共享模块把登录链路拆出来共享一遍对比 Android 和 iOS 两端的行为差异。只要这一步能稳定落地后续业务模块迁移就只是工程量的问题。建议把本文里的最小模板和测试步骤保存下来等真正做 KMP 移植时按这个顺序走一遍能省掉不少排查时间。