1. 背景与核心概念
Kotlin 早已不是那个只属于 Android 世界的“备胎”语言了。如果你还停留在“Kotlin 就是用来写 Android 的”这个认知上,那可能会错过一个极其强大的多平台开发工具链。在实际项目中,我们常常面临这样的困境:业务逻辑需要在 Web 前端、移动端(Android/iOS)和后端服务中重复实现,这不仅带来巨大的开发成本,更埋下了逻辑不一致的深坑。Kotlin Multiplatform(KMP)正是为解决这一痛点而生,它允许你用一套 Kotlin 代码来编写跨平台共享的业务逻辑。
简单来说,Kotlin 是一门运行在 Java 虚拟机(JVM)上的静态类型编程语言,由 JetBrains 开发。而 Kotlin Multiplatform 是 Kotlin 语言的一个特性,它让你能够将代码编译到多个目标平台:JVM(用于后端和 Android)、JavaScript(用于浏览器和 Node.js)、原生二进制文件(通过 Kotlin/Native 用于 iOS、macOS、Windows、Linux 等),甚至 WebAssembly(Wasm)。其核心思想是“共享逻辑,差异化 UI”:将网络请求、数据模型、业务规则等核心代码写在共享模块中,而平台特定的 UI 和系统 API 调用则写在各自的平台代码里。
为什么开发者需要掌握它?对于全栈或移动端开发者而言,它意味着代码复用率的大幅提升和逻辑一致性的根本保证。对于团队而言,它能减少重复劳动,让 iOS 和 Android 开发者基于同一套业务逻辑进行 UI 开发,极大降低沟通和联调成本。接下来,我们将用最短的路径,带你体验 Kotlin 代码如何在不同平台上“跑起来”。
2. 环境准备与版本说明
在开始我们的“15分钟环游”之前,需要确保你的开发环境已经就绪。本文的示例将基于以下环境,但请注意,Kotlin 工具链更新较快,具体版本请根据官方文档和你的项目需求调整。
核心环境:
- 操作系统:macOS、Windows 或 Linux 均可。部分平台(如 iOS 模拟器)的编译需要 macOS。
- IDE:IntelliJ IDEA Community 或 Ultimate 版。这是 Kotlin 的“亲爹”开发工具,对 KMP 支持最为完善。Android Studio(基于 IDEA)也可用于 Android 和共享模块开发。
- JDK:版本 11 或以上。推荐使用 Azul Zulu 或 Oracle JDK。
关键工具与版本:
- Kotlin 编译器:本文示例使用
1.9.22。你可以通过 IDEA 的 Kotlin 插件或项目构建脚本管理版本。 - Kotlin Multiplatform 插件:IDEA 内置支持。
- Android SDK:用于编译 Android 目标。
- Xcode:仅当需要编译和运行 iOS 目标时需要,且必须在 macOS 上。
- Node.js:用于运行 Kotlin/JS 编译后的 JavaScript 代码。
项目结构预览:我们将创建一个标准的 KMP 项目,其典型结构如下:
my-kmp-project/ ├── build.gradle.kts # 项目级构建脚本 ├── settings.gradle.kts # 项目设置 ├── shared/ # 共享模块(核心!) │ ├── build.gradle.kts │ ├── src/ │ │ ├── commonMain/ # 所有平台共享的代码和预期声明 │ │ ├── androidMain/ # Android 平台实现 │ │ ├── iosMain/ # iOS 平台实现 │ │ └── jsMain/ # JS 平台实现 │ └── ... ├── androidApp/ # Android 应用模块 ├── iosApp/ # iOS 应用模块 (Xcode项目) └── webApp/ # Web 前端项目这个结构清晰地体现了“共享模块+平台应用”的架构思想。
3. 核心语法、配置与原理拆解
在深入多平台之前,我们先快速回顾并理解 Kotlin 中支撑多平台能力的几个关键概念。
3.1 预期声明与实际声明
这是 KMP 的基石机制,用于定义跨平台 API。
expect(预期声明):在共享模块的commonMain中声明一个函数、属性或类,但不提供实现。它定义了一个契约,告诉编译器“每个目标平台都必须提供这个的实现”。actual(实际声明):在各个平台特定的源码目录(如androidMain,iosMain)中,为对应的expect声明提供具体的平台实现。
// 文件:shared/src/commonMain/kotlin/Platform.kt // 在通用代码中声明一个期待的平台信息获取函数 expect fun platformName(): String // 文件:shared/src/androidMain/kotlin/Platform.kt // 在 Android 平台上提供实际实现 actual fun platformName(): String { return "Android" } // 文件:shared/src/iosMain/kotlin/Platform.kt // 在 iOS 平台上提供实际实现 actual fun platformName(): String { return "iOS" }通过这种方式,共享模块中的通用代码可以调用platformName(),而在编译到不同平台时,链接的会是各自平台的实际实现。
3.2 多平台项目配置(Gradle KTS)
项目的跨平台能力是通过 Gradle 构建脚本(推荐使用 Kotlin DSL,即.kts后缀)配置的。核心是kotlin块中的target声明。
// 文件:shared/build.gradle.kts kotlin { // 1. 声明目标平台 androidTarget() // 会自动配置 Android 目标 jvm() // 普通的 JVM 目标(用于桌面或后端) iosX64() // 适用于 Intel 芯片的 iOS 模拟器 iosArm64() // 适用于真机 (iPhone) 或 Apple Silicon 模拟器 // iosSimulatorArm64() // 适用于 Apple Silicon 模拟器 (较新版本) js(IR) { // 使用新的 IR 编译器后端,功能更强大 browser() // 编译为在浏览器中运行 // nodejs() // 编译为在 Node.js 中运行 } // 2. 配置源代码集依赖关系(默认已关联 commonMain 到各平台) sourceSets { val commonMain by getting { dependencies { // 在这里添加所有平台共享的依赖,例如 kotlinx-coroutines-core implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } } val androidMain by getting { dependencies { // Android 平台特定的依赖 implementation("androidx.core:core-ktx:1.12.0") } } val iosMain by getting { // iOS 依赖通常通过 CocoaPods 或直接使用系统框架 } } }3.3 平台间交互与内存模型
这是 KMP 进阶时必须理解的难点。
- Kotlin/Native 并发模型:Kotlin/Native(用于编译到 iOS 等原生平台)最初有一套严格的“冻结”和单线程调度模型,以防止数据竞争。这给共享可变状态带来了挑战。
- 新版内存管理器:从 Kotlin 1.7.20 开始实验,到 1.9.20 已稳定,Kotlin/Native 采用了新的自动内存管理器,其并发模型与 JVM 和 JS 更加一致,大大简化了跨平台并发编程。在
gradle.properties中设置kotlin.native.binary.memoryModel=experimental(旧版本)或直接使用新版本即可享受这一改进。 - 线程安全:尽管内存管理器统一了,但在共享模块中编写并发代码时,仍需谨慎处理共享状态。使用
kotlinx.coroutines提供的协程原语(如Mutex,Flow)是推荐的做法。
4. 完整实战案例:一个跨平台“时间戳格式化器”
让我们通过一个完整的例子,创建一个简单的共享模块,它包含一个格式化当前时间的函数,并在 JVM、Android、iOS 和 JS 上运行它。
4.1 创建项目与共享模块
- 在 IntelliJ IDEA 中,选择New Project。
- 选择Kotlin Multiplatform下的Application模板。命名项目为
KmpQuickTour。 - 在Project Template中,选择Full-Stack Web Application或Mobile and Desktop Application以获取一个预配置了多个目标的起点。这里我们选后者。
- IDEA 会自动生成一个包含
shared,composeApp(桌面UI),androidApp,iosApp模块的项目。为了演示更纯粹,我们稍作调整。
4.2 编写共享模块核心逻辑
我们首先在共享模块中定义一个期望的时钟接口和格式化器。
// 文件:shared/src/commonMain/kotlin/com/example/kmp/TimeFormatter.kt package com.example.kmp // 预期声明:一个获取当前时间的接口 expect interface Clock { fun currentTimeMillis(): Long } // 一个纯 Kotlin 的通用格式化函数,不依赖平台 class TimeFormatter(private val clock: Clock) { fun formatCurrentTime(): String { val timeMillis = clock.currentTimeMillis() // 使用 Kotlin 标准库进行简单格式化 (仅作示例,生产环境应用更健壮的格式化) return "Current timestamp: $timeMillis" // 更复杂的格式化可以借助 kotlinx-datetime 等多平台库 } }4.3 为各平台提供实际实现
Android/JVM 实现:对于 Android 和 JVM,我们可以使用标准的System.currentTimeMillis()。
// 文件:shared/src/androidMain/kotlin/com/example/kmp/TimeFormatter.kt package com.example.kmp // JVM/Android 的实际时钟实现 actual interface Clock { actual fun currentTimeMillis(): Long = System.currentTimeMillis() }jvmMain的实现可以与此相同,或者通过androidMain和jvmMain的公共源集来共享代码。
iOS 实现:在 iOS 上,我们需要使用平台的 API。
// 文件:shared/src/iosMain/kotlin/com/example/kmp/TimeFormatter.kt package com.example.kmp import platform.Foundation.NSDate // 导入 iOS Foundation 框架 actual interface Clock { actual fun currentTimeMillis(): Long { // NSDate.timeIntervalSince1970 返回的是秒,需要乘以 1000 return (NSDate().timeIntervalSince1970 * 1000).toLong() } }JavaScript 实现:在 JS 环境中,使用Date.now()。
// 文件:shared/src/jsMain/kotlin/com/example/kmp/TimeFormatter.kt package com.example.kmp actual interface Clock { actual fun currentTimeMillis(): Long { // 使用 Kotlin/JS 的 external 声明调用 JS 的 Date.now() return Date.now().toLong() } } // 声明外部 JS 对象 external class Date { companion object { fun now(): Number } }4.4 在平台应用中调用共享代码
在 Android App 中调用:
// 文件:androidApp/src/main/kotlin/com/example/kmp/android/MainActivity.kt package com.example.kmp.android import androidx.appcompat.app.AppCompatActivity import com.example.kmp.TimeFormatter import com.example.kmp.Clock // 实际会链接到 androidMain 的实现 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val formatter = TimeFormatter(object : Clock {}) // 使用平台实现 val timeString = formatter.formatCurrentTime() Log.d("KMP", timeString) // 输出:Current timestamp: 1712... // 更新 UI... } }在 iOS App 中调用(SwiftUI):首先,共享模块编译后会生成一个 iOS 框架。在 Swift 中,你可以像使用普通框架一样导入它。
// 文件:iosApp/iosApp/ContentView.swift import SwiftUI import shared // 这是你的 Kotlin 共享模块生成的框架 struct ContentView: View { // 在 Swift 中访问 Kotlin 对象 let formatter = Shared.TimeFormatter(clock: Shared.Clock()) var body: some View { Text(formatter.formatCurrentTime()) .onAppear { print(formatter.formatCurrentTime()) // 输出到控制台 } } }在 JavaScript/Web 中调用:构建 JS 目标后,会生成.js文件。在 HTML 中引入并调用。
// 首先,确保在 jsMain 或 commonMain 中有一个方便的入口点 // 文件:shared/src/commonMain/kotlin/com/example/kmp/HelloWorld.kt fun helloWorld(): String { val formatter = TimeFormatter(object : Clock {}) return formatter.formatCurrentTime() }构建后,在 HTML 中:
<!DOCTYPE html> <html lang="en"> <head> <script src="shared.js"></script> <!-- 引入编译后的 Kotlin/JS 代码 --> </head> <body> <script> console.log(helloWorld()); // 调用 Kotlin 函数,输出到浏览器控制台 document.body.innerText = helloWorld(); </script> </body> </html>4.5 编译与运行
- 编译所有目标:在 IDEA 的 Gradle 工具窗口中,找到
shared模块下的build任务,或直接运行./gradlew build。 - 运行 Android:选择
androidApp配置,运行到模拟器或真机。 - 运行 iOS:在 macOS 上,用 Xcode 打开
iosApp目录下的.xcodeproj文件,选择模拟器运行。 - 运行 JS:执行
./gradlew jsBrowserRun,Gradle 会启动一个开发服务器并打开浏览器。
5. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
编译错误:Unresolved reference: expect | 1. 代码放错了源码集目录。 2. 对应的 actual实现缺失。 | 1. 检查expect声明是否在commonMain下。2. 确保每个目标平台( androidMain,iosMain等)都提供了actual实现。 |
iOS 编译失败:找不到platform.Foundation | Kotlin/Native 与 iOS 框架链接问题。 | 1. 确保iosMain依赖了正确的包。2. 在 shared的build.gradle.kts的iosMain依赖中添加implementation(kotlin(\"stdlib\"))。 |
| 运行时错误:iOS 上调用 Kotlin 代码崩溃 | 内存访问或并发问题,特别是涉及冻结对象。 | 1. 确保使用新版内存管理器(Kotlin >=1.9.20)。 2. 避免在共享模块中直接传递复杂的、可变的 Kotlin 对象到 Swift。使用 @Serializable数据类或纯函数接口。 |
| JS 输出文件很大 | 包含了整个 Kotlin 标准库。 | 1. 使用kotlin.js.compiler.production=true(Gradle 属性)进行生产构建。2. 使用 webpack等工具进行代码分片和压缩。 |
| Gradle 同步慢或失败 | 网络问题或仓库配置错误。 | 1. 检查build.gradle.kts中的仓库是否配置正确(如mavenCentral())。2. 使用国内镜像源。 3. 尝试离线模式或清理 Gradle 缓存。 |
actual实现无法访问平台特定 API | 在错误的sourceSet中尝试调用平台 API。 | 确保平台 API 的调用只存在于对应的平台源码集(如androidMain,iosMain)中。通用代码(commonMain)只能调用expect声明或纯 Kotlin 库。 |
6. 最佳实践与工程建议
将 Kotlin 用于多平台开发不仅仅是技术可行,更需要良好的工程实践来保证项目的可维护性和开发效率。
清晰架构,职责分离
- 共享模块是核心:仅将真正的业务逻辑放入共享模块。例如:数据模型(
data class)、网络请求定义(使用ktor-client等多平台库)、仓库层接口、Use Case 等。 - 平台专属代码放两边:UI、系统服务调用(如通知、传感器)、平台特定的存储(
SharedPreferenceson Android,UserDefaultson iOS)都应留在各自的应用模块中。
- 共享模块是核心:仅将真正的业务逻辑放入共享模块。例如:数据模型(
依赖管理策略
- 优先使用多平台库:对于网络(
ktor-client)、序列化(kotlinx.serialization)、协程(kotlinx-coroutines-core)、日期时间(kotlinx-datetime),务必使用其多平台版本。 - 谨慎引入平台依赖:在共享模块中,通过
expect/actual机制来抽象平台依赖。避免在commonMain中直接引入androidx.*或UIKit等库。
- 优先使用多平台库:对于网络(
API 设计面向契约
- 使用
expect接口或抽象类来定义平台需要实现的契约。保持接口精简,聚焦能力而非实现细节。 - 考虑使用
interface和dependency injection(如 Koin 或 Kodein-DI 的多平台版本)来管理平台实现的注入,提高代码的可测试性。
- 使用
iOS 交互优化
- 简化交互对象:在 Kotlin 和 Swift 之间传递的数据结构应尽可能简单。使用
@Serializable标记的数据类,它们会被编译为易于 Swift 理解的格式。 - 处理并发:如果共享模块使用协程,在 iOS 端暴露挂起函数(
suspend)时,Kotlin/Native 编译器会生成带有completionHandler的 Objective-C 方法,方便在 Swift 中通过回调或async/await(Swift 5.5+)调用。
- 简化交互对象:在 Kotlin 和 Swift 之间传递的数据结构应尽可能简单。使用
构建与持续集成
- 配置缓存:启用 Gradle 构建缓存(
org.gradle.caching=true)和 Kotlin 编译缓存,大幅提升构建速度。 - CI/CD 流水线:为每个目标平台设置独立的构建和测试任务。对于 iOS,需要在 macOS 代理上运行。
- 版本对齐:使用
kotlin(\"bom\")或手动对齐所有 Kotlin 相关依赖(kotlinx-*系列)的版本,避免因版本不兼容导致的诡异问题。
- 配置缓存:启用 Gradle 构建缓存(
渐进式采用
- 不要试图一次性将整个应用重写为 KMP。可以从一个独立的、相对稳定的业务模块(如用户认证、配置管理、分析统计 SDK)开始,将其重构为共享模块,供各平台应用集成。
- 先实现 Android 和 JVM 目标,再逐步扩展到 iOS 和 JS,以控制风险。
7. 总结与学习路线
通过以上步骤,我们完成了一次 Kotlin 代码在 JVM、Android、iOS 和 JS 平台上的快速穿梭。核心在于理解expect/actual机制和 Gradle 的多平台配置。你创建了一个共享的格式化器,并在四个不同的环境中执行了它,这证明了“一次编写,多处运行”的可行性。
本文关键点回顾:
- 概念:Kotlin Multiplatform 用于共享业务逻辑,UI 保持原生。
- 核心机制:
expect声明契约,actual提供平台实现。 - 配置:在
build.gradle.kts中通过kotlin { targets { ... } }声明目标平台。 - 开发流程:在
shared模块写通用逻辑和expect,在各平台源码集写actual实现和 UI。 - 交互:iOS 通过生成的框架调用,JS 通过编译后的
.js文件调用。
下一步学习路线:
- 深入官方文档:仔细阅读 Kotlin 官方多平台文档 ,这是最权威的信息源。
- 探索多平台库:学习使用
ktor-client(网络)、kotlinx.serialization(序列化)、sqldelight(数据库)等明星多平台库来构建更复杂的共享功能。 - UI 共享实践:如果你对共享 UI 感兴趣,可以研究Compose Multiplatform,它允许你用相同的声明式 UI 代码构建 Android、桌面和 Web 应用(iOS 仍处于 Alpha 阶段)。
- 实战小项目:尝试用 KMP 构建一个简单的笔记应用或天气应用,涵盖数据层共享和基础 UI 调用。
- 关注社区:关注 Kotlin 官方博客、Kotlin Slack 频道和 GitHub 上的热门 KMP 项目,了解最佳实践和新动态。
Kotlin Multiplatform 正在走向成熟,尤其是在 Compose Multiplatform 的推动下,其生态日益繁荣。虽然初期会遇到一些工具链和生态上的小挑战,但它为减少重复开发、保证逻辑一致性所提供的价值是巨大的。开始你的第一个 KMP 模块,从共享一个工具类或网络模型开始,逐步体验多平台开发的魅力吧。如果在实践中遇到具体问题,多查阅文档和社区讨论,大多数坑都已经有前人踩过并提供了解决方案。