
1. 背景与核心概念为什么要做 Kotlin Multiplatform 移植1.1 从“移植”说起嵌入式移植与跨平台移植的共同点看到标题里的“移植”两个字很多读者第一反应可能是嵌入式方向比如 FreeRTOS 移植到 STM32、LVGL 移植、LWIP 移植、ADI 的 AD2S1210 驱动移植这一类的文章。这类移植工作有一个共同特征把一套已经存在的代码或者操作系统内核通过适配硬件平台相关代码让它能在新的 MCU 或 SoC 上运行起来。搜索热词里大量的“freertos移植”“lvgl移植stm32”“cherryusb移植教程”都属于这个范畴。但今天这篇文章要讲的“移植”方向不同技术栈也完全不同。我们讨论的是Kotlin Multiplatform简称 KMP的移植也就是把一个原本只存在于 Android 端的项目或模块迁移到 Kotlin Multiplatform 架构中让同一份业务逻辑代码可以同时运行在 Android、iOS、桌面端等多个平台之上。这两种“移植”虽然看上去风马牛不相及但核心思想是一致的原有代码经过平台适配后能在目标平台上运行平台相关的部分要被隔离出来用一套接口描述再分别为不同平台实现平台无关的代码尽量复用减少重复劳动。理解了这一点再去对照 Kotlin Multiplatform 的 expect/actual 机制你会发现两者的设计哲学惊人相似。1.2 Kotlin Multiplatform 到底是什么Kotlin Multiplatform 是 JetBrains 推出的一套跨平台开发方案。它允许你用 Kotlin 编写业务逻辑代码然后通过 Gradle 构建出面向不同平台的目标产物Android生成 AAR 或直接作为 Android 模块引用iOS生成 framework 或通过 CocoaPods 集成桌面端生成 JVM 平台代码JavaScript/Web生成 JS 目标产物还有一些非主流目标平台如 Linux、Windows、Wasm 等。它的核心优势是业务逻辑、数据模型、网络请求、本地存储、状态管理这些代码可以只写一遍。而 UI 层则保持各平台原生实现比如 Android 继续用 Jetpack ComposeiOS 继续用 SwiftUI 或 UIKit桌面端可以根据需要选择。这里要注意区分一个概念Kotlin Multiplatform 并不是“一次编写到处运行”的 UI 跨平台方案。它强调的是“共享逻辑原生体验”。业务逻辑共享UI 保持原生。1.3 项目移植与跨平台方案选型的思考在做任何 KMP 移植之前必须要回答一个问题项目适不适合做 KMP 移植一个典型的适合做 KMP 移植的项目通常具备以下特征业务逻辑复杂包含大量数据解析、状态管理、网络请求和本地缓存团队同时维护 Android 和 iOS 两个端出现了逻辑不一致的维护痛点两端代码中业务逻辑部分占比高UI 部分相对简单团队有 Kotlin 语言基础对 Gradle 构建体系熟悉。反过来如果整个项目几乎全是 UI 展示业务逻辑很少那么 KMP 移植的收益就不大强行迁移反而会增加构建链路的复杂度。本文以“星球突击队”项目为例这是一个假设的项目名称你可以把它理解成一个包含用户登录、任务列表、积分系统、排行榜等模块的典型业务项目。我们围绕它的核心业务模块来做 KMP 移植重点覆盖数据模型、网络层、本地存储层、依赖注入和测试这几个方面。2. 环境准备与版本说明2.1 开发环境清单KMP 开发比起普通的 Android 开发对工具链的完整性要求更高因为需要同时管理 Android 与 iOS 两个平台甚至还需要 Kotlin/Native 的编译器参与。下面是建议的环境清单工具说明注意事项JDK建议 JDK 17 及以上Kotlin 2.0 之后的版本对 JDK 版本有要求Android Studio建议使用较新的稳定版本需要安装 Kotlin 插件新版本已内置XcodeiOS 构建与 framework 集成使用仅在需要编译 iOS 目标时需要Kotlin Multiplatform 插件通过 Gradle 插件管理版本由项目 Gradle 决定Gradle建议 8.5 及以上结合 AGP 版本一起确认CocoaPods可选用于 iOS 工程集成不是必须也可以用直接 framework 方式2.2 版本兼容性问题KMP 开发中版本兼容性是最容易踩坑的点。Kotlin 版本、AGPAndroid Gradle Plugin版本、Gradle 版本三者之间存在对应关系。如果版本不匹配通常会出现类似下面的报错The Android Gradle plugin requires Gradle 8.x. Current version is 7.x.或者Kotlin Gradle plugin requires a newer Gradle version.所以配置环境时不要盲目追求最新要参考官方兼容性表格选择稳定组合。以本文示例环境为例可以用这样一组版本组合kotlin.version2.0.20 agp.version8.5.0 gradle.version8.9这组版本是当前较常见的稳定组合。但注意版本更新很快等你动手的时候可能有更新的稳定版本出现。建议优先选用你本地 Android Studio 能识别、Gradle 能正常解析的版本组合。2.3 仓库依赖配置KMP 项目需要依赖多个仓库源包括 Google 的 Maven 仓库、Maven Central 以及 JetBrains 的仓库。在根目录的settings.gradle.kts中配置如下// 文件路径settings.gradle.kts pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositories { google() mavenCentral() } }这里的核心在于pluginManagement和dependencyResolutionManagement两个配置块。前者负责管理插件依赖的解析后者负责管理项目依赖的解析。KMP 构建中会拉取大量 Kotlin/Native 相关依赖如果网络不稳定建议配置镜像仓库。3. Kotlin Multiplatform 核心架构拆解3.1 模块划分shared 还是多模块一个 KMP 项目最常见的组织结构是在原有 Android 项目旁边新增一个shared模块所有的跨平台业务逻辑都放在这里。ProjectRoot/ ├── androidApp/ # Android 宿主应用 ├── iosApp/ # iOS 宿主应用 ├── shared/ # KMP 共享模块 │ ├── src/ │ │ ├── commonMain/ # 平台无关代码 │ │ ├── androidMain/ # Android 平台实现 │ │ └── iosMain/ # iOS 平台实现 │ └── build.gradle.kts └── settings.gradle.kts对于“星球突击队”这类中小型项目一个shared模块就够了。如果项目特别庞大可以考虑把shared拆成多个子模块例如shared:core、shared:network、shared:data但这样会显著增加配置复杂度不建议刚上手 KMP 时这么做。3.2 expect 与 actual平台差异的解决方案KMP 中最重要的关键字是expect和actual。expect用于在commonMain中声明一个期待平台实现的函数、类或属性actual用于在androidMain、iosMain等平台源码集中定义对应的实际实现。举一个最简单的例子。假设我们要获取当前设备的平台名称// 文件路径shared/src/commonMain/kotlin/com/example/planet/Platform.kt package com.example.planet expect fun getPlatformName(): String然后分别在 Android 和 iOS 平台实现// 文件路径shared/src/androidMain/kotlin/com/example/planet/Platform.android.kt package com.example.planet actual fun getPlatformName(): String { return Android }// 文件路径shared/src/iosMain/kotlin/com/example/planet/Platform.ios.kt package com.example.planet actual fun getPlatformName(): String { return iOS }expect/actual的适用场景非常明确只有那些平台之间确实存在 API 差异无法用统一代码覆盖的能力才需要用它。比如读取设备信息、获取当前时间戳、访问系统偏好设置、实现加密等。有一点要特别提醒expect/actual不是让你在commonMain里面写一堆声明然后到各个平台去写对应的实现。如果某个能力有第三方库提供了 KMP 支持优先用库而不是自己写expect/actual。3.3 源码集Source Set理解KMP 的源码集是理解这个架构的关键。commonMain/commonTest平台无关的代码和测试androidMain/androidUnitTestAndroid 平台实现和单元测试iosMain/iosTestiOS 平台实现和测试。在build.gradle.kts中需要为每个目标平台声明 iOS 目标。由于 iOS 模拟器和真机使用的 CPU 架构不同这里通常要声明iosArm64真机和iosSimulatorArm64模拟器两个目标kotlin { androidTarget() listOf( iosArm64(), iosSimulatorArm64() ).forEach { iosTarget - iosTarget.binaries.framework { baseName Shared isStatic true } } sourceSets { commonMain.dependencies { // 公共依赖 } androidMain.dependencies { // Android 平台依赖 } iosMain.dependencies { // iOS 平台依赖 } } }这里的baseName Shared决定了生成的 framework 名称后续在 iOS 工程中需要用这个名字来引入。3.4 与嵌入式移植的对比理解为了帮助一些从嵌入式转过来的读者理解这里做一个类比嵌入式移植Kotlin Multiplatform 移植把 RTOS 内核通过汇编/C 适配到 MCU把业务逻辑通过 Kotlin 编译到 Android/iOSportmacro.h等头文件适配expect/actual声明与实现HAL 层隔离硬件差异源码集隔离平台 API 差异编译成目标平台固件编译成 AAR / framework交叉编译工具链Kotlin/Native 编译器不能说两者完全等价但设计思路是相通的尽可能把平台相关的代码隔离到边界让公共代码保持纯粹。4. 实战星球突击队 KMP 移植完整流程4.1 项目结构搭建假设我们有一个 Android 项目“星球突击队”原本的包结构大致是com.example.planet/ ├── data/ │ ├── model/ # 数据模型 │ ├── network/ # 网络请求 │ └── repository/ # 数据仓库 ├── domain/ # 业务逻辑 └── ui/ # Android 界面做 KMP 移植时我们的第一步不是急着写代码而是先确定哪些代码是平台无关的哪些需要留在 Android 端。适合迁移到commonMain的代码数据模型Bean/DTO/Entity网络请求客户端本质上是 HTTP 调用和 JSON 解析数据仓库层Repository负责数据来源的切换和缓存策略业务用例UseCase纯逻辑计算本地存储的接口定义依赖注入的容器定义单元测试。保留在 Android 端、由 Android 原生实现的内容Activity / Fragment / Compose 界面Android 专属的 UI 组件Android 系统服务调用部分依赖 Android SDK 的工具类。4.2 创建 shared 模块在根目录的settings.gradle.kts中注册模块// 文件路径settings.gradle.kts include(:androidApp) include(:shared)创建shared/build.gradle.kts// 文件路径shared/build.gradle.kts plugins { kotlin(multiplatform) kotlin(plugin.serialization) version 2.0.20 id(com.android.library) } kotlin { androidTarget { compilations.all { kotlinOptions { jvmTarget 17 } } } listOf( iosArm64(), iosSimulatorArm64() ).forEach { iosTarget - iosTarget.binaries.framework { baseName Shared isStatic true } } sourceSets { commonMain.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) } androidMain.dependencies { implementation(io.ktor:ktor-client-okhttp:2.3.12) } iosMain.dependencies { implementation(io.ktor:ktor-client-darwin:2.3.12) } commonTest.dependencies { implementation(kotlin(test)) } } } android { namespace com.example.planet.shared compileSdk 34 defaultConfig { minSdk 24 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } }这里有几个关键配置点需要逐一解释kotlin(multiplatform)是 KMP 的入口插件没有它整个项目无法运行。kotlin(plugin.serialization)用于 JSON 序列化它对应代码里Serializable注解。androidTarget生成的 AAR 可以被 Android 工程直接依赖。iosArm64和iosSimulatorArm64是两个独立的 iOS 目标分别对应真机和 Apple Silicon 模拟器。Ktor 引擎需要按平台单独添加Android 用 OkHttpiOS 用 Darwin。4.3 数据模型迁移在原有 Android 工程中数据模型可能是这样的// 原 Android 工程中的 Task.kt data class Task( val id: String, val title: String, val points: Int, val status: String )在 KMP 中我们把它迁移到commonMain并加上Serializable注解让它具备 JSON 序列化能力// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/model/Task.kt package com.example.planet.data.model import kotlinx.serialization.Serializable Serializable data class Task( val id: String, val title: String, val points: Int, val status: String ) Serializable data class UserProfile( val userId: String, val nickname: String, val avatarUrl: String?, val totalPoints: Int ) Serializable data class ApiResponseT( val code: Int, val message: String, val data: T? null )这里使用泛型包装类ApiResponseT的好处是后端返回统一格式时可以通过泛型映射到具体的业务模型。在 KMP 中泛型与Serializable结合不会有任何问题ApiResponseTask可以直接被序列化框架处理。4.4 网络层移植网络层是整个移植过程中最核心、也最容易出问题的一部分。在原先的 Android 工程中你可能用的是 Retrofit OkHttp Gson 的组合。但 Retrofit 不支持 KMP因此网络层需要换用Ktor Client。Ktor Client 是 JetBrains 出品的网络框架支持 KMP底层引擎可以按平台切换Android 上使用 OkHttp 引擎iOS 上使用 Darwin 引擎。首先定义网络请求接口// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/network/ApiService.kt package com.example.planet.data.network import com.example.planet.data.model.ApiResponse import com.example.planet.data.model.Task import com.example.planet.data.model.UserProfile import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter class ApiService( private val client: HttpClient ) { suspend fun getUserProfile(userId: String): UserProfile { val response: ApiResponseUserProfile client.get( https://api.planet-game.com/api/users/$userId ).body() return response.data ?: throw IllegalStateException(用户数据为空) } suspend fun getTasks(userId: String): ListTask { val response: ApiResponseListTask client.get( https://api.planet-game.com/api/tasks ) { parameter(userId, userId) }.body() return response.data ?: emptyList() } }注意这里的body()是 Ktor Client 提供的扩展函数它依赖 ContentNegotiation 插件完成 JSON 反序列化。接下来创建 HttpClient 的工厂函数方便在各平台共享配置// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/network/HttpClientFactory.kt package com.example.planet.data.network import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json fun createHttpClient(): HttpClient { return HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys true isLenient true encodeDefaults true }) } install(HttpTimeout) { requestTimeoutMillis 15_000 connectTimeoutMillis 15_000 socketTimeoutMillis 15_000 } } }Json的配置项需要解释一下ignoreUnknownKeys true后端返回多余字段时不解析失败isLenient true允许 JSON 格式不严格时兼容处理encodeDefaults true序列化时把默认值也写入 JSON。这里的createHttpClient()没有指定平台引擎是因为 Ktor 的HttpClient()构造函数会自动探测 classpath 中可用的引擎。在 Android 上它发现ktor-client-okhttp在 iOS 上它发现ktor-client-darwin。4.5 本地存储与数据仓库本地存储方面如果使用 Android 的 SharedPreferences直接迁移是不可能的因为android.content.Context是 Android 专属 API。这里可以使用一个 KMP 兼容的库multiplatform-settings。在commonMain中加入依赖implementation(com.russhwolf:multiplatform-settings:1.2.0)在commonMain中实现一个简单的 Token 存储// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/local/SettingsManager.kt package com.example.planet.data.local import com.russhwolf.settings.Settings import com.russhwolf.settings.get class SettingsManager( private val settings: Settings ) { fun saveToken(token: String) { settings.putString(KEY_TOKEN, token) } fun getToken(): String? { return settings.getStringOrNull(KEY_TOKEN) } fun clearToken() { settings.remove(KEY_TOKEN) } private companion object { const val KEY_TOKEN token } }这里有一个问题Settings是从哪里来的Android 端需要基于Context创建iOS 端需要基于NSUserDefaults创建。这就必须用到expect/actual来解决平台差异。在commonMain中定义创建入口// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/local/SettingsFactory.kt package com.example.planet.data.local import com.russhwolf.settings.Settings expect fun createSettings(): SettingsAndroid 实现// 文件路径shared/src/androidMain/kotlin/com/example/planet/data/local/SettingsFactory.android.kt package com.example.planet.data.local import android.content.Context import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings private lateinit var appContext: Context fun initSettings(applicationContext: Context) { appContext applicationContext } actual fun createSettings(): Settings { return SharedPreferencesSettings( appContext.getSharedPreferences(planet_settings, Context.MODE_PRIVATE) ) }iOS 实现// 文件路径shared/src/iosMain/kotlin/com/example/planet/data/local/SettingsFactory.ios.kt package com.example.planet.data.local import com.russhwolf.settings.Settings import com.russhwolf.settings.NSUserDefaultsSettings import platform.Foundation.NSUserDefaults actual fun createSettings(): Settings { return NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults) }数据仓库层的代码则可以完全放在commonMain中因为它只依赖我们定义好的接口// 文件路径shared/src/commonMain/kotlin/com/example/planet/data/repository/UserRepository.kt package com.example.planet.data.repository import com.example.planet.data.local.SettingsManager import com.example.planet.data.model.Task import com.example.planet.data.model.UserProfile import com.example.planet.data.network.ApiService class UserRepository( private val apiService: ApiService, private val settingsManager: SettingsManager ) { suspend fun getUserProfile(userId: String): UserProfile { return apiService.getUserProfile(userId) } suspend fun getTaskList(userId: String): ListTask { return apiService.getTasks(userId) } fun logout() { settingsManager.clearToken() } }4.6 依赖注入方案在 KMP 中依赖注入常见选择有 Koin 和 Kotlin-inject。对于中小型项目Koin 的易用性更好它对 KMP 的支持也比较成熟。在commonMain中添加依赖implementation(io.insert-koin:koin-core:4.0.0)然后定义模块// 文件路径shared/src/commonMain/kotlin/com/example/planet/di/AppModule.kt package com.example.planet.di import com.example.planet.data.network.ApiService import com.example.planet.data.network.createHttpClient import com.example.planet.data.repository.UserRepository import com.russhwolf.settings.Settings import org.koin.core.module.Module import org.koin.core.module.dsl.singleOf import org.koin.dsl.module fun platformModule(): Module module { singleSettings { createSettings() } } fun appModule(): Module module { single { createHttpClient() } single { ApiService(get()) } singleOf(::UserRepository) }然后在 Android Application 初始化时装配// 文件路径androidApp/src/main/java/com/example/planet/PlanetApplication.kt class PlanetApplication : Application() { override fun onCreate() { super.onCreate() initSettings(applicationContext) startKoin { modules( appModule(), platformModule() ) } } }iOS 端则需要找一个应用启动的时机来初始化 Koin通常在 SwiftUI 的App.init中调用 KMP 暴露的初始化函数。这里要注意的是platformModule()返回的模块中包含了createSettings()调用而这个函数是expect函数必须在实际平台的源码集中有对应的actual实现否则编译器直接报错。4.7 iOS 工程集成完成 shared 模块的构建后需要把生成物集成到 iOS 工程中。有两种主流方案方案一CocoaPods 集成在shared目录下创建build.gradle.kts中的 CocoaPods 配置kotlin { cocoapods { summary Shared module for Planet Project homepage https://example.com version 1.0 ios.deploymentTarget 15.0 framework { baseName Shared } } }然后在 iOS 工程的Podfile中引用platform :ios, 15.0 target iosApp do pod Shared, :path ../shared end终端执行pod install后Xcode 工程会自动关联 framework。方案二直接集成 framework运行 Gradle 任务生成 framework./gradlew :shared:linkDebugFrameworkIosSimulatorArm64然后在 Xcode 工程中手动引入生成的Shared.framework。这种方式灵活但每次修改 shared 代码后都需要手动重新生成比较麻烦。实际项目建议使用 CocoaPods 方案。4.8 运行与验证Android 端可以直接运行 Android App通过断点或者日志确认网络请求和数据解析是否正常。iOS 端运行前先确认以下事项shared模块的 framework 生成成功Podfile 和 Pods 目录已经同步Xcode 工程能够通过编译Swift 调用的方法签名与 Kotlin 导出到 Objective-C 的接口是否匹配。在 Swift 中调用 Kotlin 代码的典型写法import Shared let repository AppModuleKt.createUserRepository() let profile try await repository.getUserProfile(userId: 12345) print(profile.nickname)这里有一个常见坑Kotlin 的suspend函数在 Swift 中会表现为带有completionHandler的方法或者在新版本 Kotlin 中转换成async方法。如果你看到类似下面的报错Missing argument for parameter completionHandler in call说明你需要用闭包方式调用repository.getUserProfile(userId: 12345) { profile, error in if let profile profile { print(profile.nickname) } }5. 常见问题与排查思路5.1 常见问题汇总问题现象常见原因解决思路Gradle 构建失败提示 Kotlin 版本冲突Kotlin 插件版本与 AGP 版本不兼容查看官方兼容表统一 Kotlin、AGP、Gradle 版本expect函数没有找到对应actual平台源码集文件中漏写实现检查androidMain和iosMain中是否有对应声明iOS 编译失败提示找不到 Frameworkshared 模块 framework 未生成成功先执行./gradlew :shared:build确认构建成功Ktor 请求在 iOS 上失败缺少平台引擎依赖确认iosMain中已添加ktor-client-darwinSwift 调用 Kotlin suspend 函数报参数错误对挂起函数导出格式不熟悉使用 completionHandler 或 async/await 方式调用Android 原工程中 Gson 模型无法解析LocalDate 等类型序列化问题改用 kotlinx.serialization 并手写 SerializerCocoaPodspod install找不到 Shared podPodspec 未生成或路径错误先运行 Gradle 同步任务生成 podspeciOS 模拟器运行卡住构建了错误 CPU 架构检查是iosArm64还是iosSimulatorArm64Koin 报未找到依赖模块注册顺序有问题确保依赖的模块在调用前已经被加载5.2 典型排错案例iOS 真机闪退在 iOS 真机上运行时如果 App 启动几秒后直接闪退控制台输出类似下面的日志Terminating app due to uncaught exception NSInvalidArgumentException, reason: application is not in foreground这种问题通常是 Koin 初始化时机不对或者 Settings 的创建没有准备好。排查顺序建议为打开 Xcode 的 Console 面板查看完整崩溃堆栈确认startKoin是否在 App 启动早期被调用确认createSettings()的 iOS 实现是否返回了合法的NSUserDefaultsSettings如果使用了lateinit var确认是否发生了“used before initialized”的情况。5.3 典型排错案例JSON 解析失败在 Android 端构建正常但在 iOS 端调用网络接口时解析失败提示kotlinx.serialization.SerializationException: Expected string as element这类问题多半是后端返回的字段类型不匹配或者 JSON 字段大小写与 Kotlin 模型不一致。建议在Json配置中开启ignoreUnknownKeys对可能为空的字段使用可空类型用SerialName注解映射不同命名字段在开发阶段把响应体先打印出来确认字段名。5.4 排查检查清单当项目出现无法定位的问题时按下面的清单逐步检查Gradle 同步是否通过commonMain、androidMain、iosMain三个源码集是否都有内容expect/actual是否成对出现iOS 目标是否注册完整网络库引擎是否按平台添加序列化注解是否遗漏CocoaPods 是否重新安装Xcode 工程缓存是否清理Kotlin 与 AGP 版本组合是否稳定是否查阅过官方迁移指南。6. 最佳实践与工程建议6.1 代码结构分层KMP 项目成功的关键之一是分层清晰。建议采用以下分层数据模型层只包含Serializabledata class不依赖其他模块网络层负责 HTTP 请求和响应解析数据仓库层聚合多个数据源对外提供统一接口业务逻辑层处理具体业务规则不感知 UI平台适配层集中放置expect/actual代码。每层之间通过接口通信减少耦合。实际项目中很多团队出现问题就是因为把网络请求直接写进了 UI 层导致移植时无法拆分。6.2 命名规范在 KMP 工程中命名规范需要格外严格因为同一个类可能会被 Kotlin、Swift、Java 三种语言引用。包名统一使用小写避免下划线类名和函数名遵循驼峰命名常量使用大写加下划线暴露给 Swift 的 API避免使用is、get、set等容易与 Objective-C 冲突的前缀expect函数名字尽量带上平台含义比如createSettings、getPlatformName不要过度抽象。6.3 版本管理建议在根项目的gradle/libs.versions.toml中集中管理版本号方便统一升级。比如[versions] kotlin 2.0.20 agp 8.5.0 ktor 2.3.12 koin 4.0.0 kotlinxSerialization 1.7.1 [libraries] ktor-client-core { module io.ktor:ktor-client-core, version.ref ktor } ktor-client-okhttp { module io.ktor:ktor-client-okhttp, version.ref ktor } ktor-client-darwin { module io.ktor:ktor-client-darwin, version.ref ktor }版本升级时优先升级 Kotlin 版本然后按官方兼容表调整 AGP 和 Gradle 版本。KMP 相关的第三方库例如 Ktor、Koin、multiplatform-settings要尽量选择与 Kotlin 版本兼容较新的版本。6.4 日志与调试KMP 的调试Android 端可以直接用 LogcatiOS 端则会麻烦一些。建议在 commonMain 中封装一个日志工具// 文件路径shared/src/commonMain/kotlin/com/example/planet/util/Logger.kt package com.example.planet.util enum class LogLevel { DEBUG, INFO, WARN, ERROR } expect fun logMessage(level: LogLevel, tag: String, message: String)Android 实现使用android.util.LogiOS 实现使用NSLog。这样在 shared 中的任何代码都可以统一打日志不暴露平台差异。6.5 性能与包体积考虑KMP 生成的 framework 会增加 App 的二进制体积尤其是 Debug 版本。建议iOS 端使用静态 framework减少启动开销发布版本开启 R8/ProGuard 混淆只注册实际用到的 iOS 目标避免在 shared 中引过重的第三方库尽量减少expect/actual的数量因为每一对都会增加代码生成量。6.6 安全边界网络请求涉及用户数据必须注意安全问题。Token 不会硬编码在代码中统一走 SettingsManager 读取网络请求必须使用 HTTPS不信任自签名证书日志中不得打印 Token、密码等敏感信息涉及账号注销、积分变更等操作必须二次校验上线前要对 shared 模块做安全扫描确认没有把敏感逻辑暴露到 Objective-C 头文件。7. 总结与下一步学习建议到这里“星球突击队”的 Kotlin Multiplatform 移植核心流程已经梳理清楚了。我们从项目背景与概念出发介绍了 KMP 的环境搭建、模块划分、expect/actual 机制、网络层与本地存储的移植再到 iOS 工程的集成方式最后给出了一批常见问题和工程建议。如果你现在准备在自己的项目里尝试 KMP我的建议是不必一步到位把整个项目搬过去。可以先从“星球突击队”的架构模式中选择一个业务边界清晰的模块做试点。比如先把用户信息模块迁移到 sharedAndroid 和 iOS 两端都调用同一份 Repository 代码。验证稳定后再逐步扩展网络层和任务模块。以“模块渐进替换”的方式引入 KMP比一次性大规模重构的风险要小得多。但如果你希望少踩一些坑记住几个关键点第一优先锁定 Kotlin、AGP、Gradle 的版本组合不要轻易改动第二把网络层引擎和本地存储这些平台相关代码尽早用 Factory 模式封装起来不要在业务代码里直接引用平台 API第三建立一个自动化脚本每次修改 shared 代码后自动生成 framework 并同步到 iOS 工程能省下大量手动操作时间。最后想说KMP 的学习曲线不是陡在 Kotlin 语法上而是陡在构建链路和平台差异理解上。建议先跑通一个最小项目亲手完成一次 Android 与 iOS 的调用闭环再研究复杂的业务迁移。这个从“能跑”到“会设计”的过程其实就是最好的工程能力成长路径。