嘿,朋友!如果你正盯着屏幕发呆,手里攥着安卓开发的简历,心里却羡慕着那些能同时搞定 iOS 和 Android 的“全栈大神”,那你来对地方了。别被“全栈”这两个字吓跑,今天我们要聊的不是什么高深莫测的黑魔法,而是一把真正的瑞士军刀——Kotlin Multiplatform (KMP)。
想象一下这个场景:你在家里写了一套复杂的逻辑代码,比如一个计算房贷利息的算法,或者一个处理用户登录状态的管理器。以前,你得把这套逻辑抄两遍,一遍给 Java/Kotlin 安卓团队,一遍给 Swift iOS 团队。结果呢?两边改 Bug 改得头秃,还总因为逻辑不一致被产品经理骂。现在,有了 KMP,你只写一次,安卓能用,苹果也能用。是不是感觉世界突然清净了不少?
咱们不讲那些枯燥的定义,直接切入正题。我会像带你逛自家后院一样,一步步带你搭建起这个跨平台的世界。哪怕你是零基础,只要跟着我的节奏走,保证你能写出第一个能同时在手机和电脑上跑起来的 Demo。
第一步:为什么要折腾“一套代码,两端运行”?
在开始敲代码之前,咱们先聊聊初心。很多刚入行的开发者会问:“我用 Flutter 不香吗?为什么非要学 Kotlin?”
这就好比选交通工具。Flutter 像是一辆组装好的电动车,电池电机都是现成的,跑得快,但如果你想换个发动机,或者深入到底盘去修个零件,你会发现外壳焊死了,很难受。而 Kotlin Multiplatform 更像是给你提供了一套通用的发动机和传动系统。你的 UI 界面依然由安卓原生的 Jetpack Compose 或苹果原生的 SwiftUI 负责(这样用户体验才最原生嘛),但你核心的业务逻辑、数据层、网络请求,统统可以用 Kotlin 写。
这意味着什么?意味着你的安卓团队可以学习 Swift 的基础知识来理解项目,iOS 团队也可以看看 Kotlin 代码。更重要的是,Kotlin 是 JetBrains 亲儿子,它在安卓生态里如鱼得水,而在苹果生态里,通过 Kotlin/Native,它能把代码编译成机器码,性能几乎没有损耗。
咱们举个真实的例子。假设你要做一个电商 App。商品列表的展示,安卓用 Compose,iOS 用 SwiftUI,这没问题,因为这是 UI 层,必须原生才能丝滑。但是,当用户点击“加入购物车”时,后端 API 怎么调?数据怎么缓存?订单状态怎么校验?这些全是纯逻辑。以前,安卓写个 Retrofit 请求,iOS 写个 URLSession 请求,逻辑还得再写一遍。现在,你写一个 ProductRepository 类,里面封装好所有逻辑,安卓引用它,iOS 引用它。改需求?只改这一个文件,两端自动同步。这种爽快感,用过就回不去了。
第二步:环境搭建——别让安装软件成为你的噩梦
好了,理论聊完了,咱们动手。首先,你得有个“战场”。对于初学者来说,IntelliJ IDEA 是最友好的选择,因为它对 Kotlin 的支持是顶级的。当然,Android Studio 也可以,但在配置多平台项目时,IDEA 的插件体验稍微顺滑一点。
1. 安装 JDK 和 Gradle
KMP 是基于 Gradle 构建的,所以 JDK 是必须的。去 Oracle 官网或者 Adoptium 下载一个 JDK 17 或更高版本。别担心,现在的 IDE 大多自带了 SDK 管理,你只需要确保环境变量配好就行。
2. 创建新项目
打开 IntelliJ IDEA,点击 “New Project”。在左侧菜单找到 “Kotlin”,然后选择 “Multiplatform Library”。注意,是 Library 不是 Application。为什么?因为我们通常不直接写跨平台的 UI(虽然技术上可行,但社区主流做法是逻辑共享)。我们创建一个库,然后在安卓和 iOS 项目中分别引用它。
给项目起个名字,比如 shared-core。包名随便起,比如 com.yourname.shared。
这时候,你会看到项目结构里有几个关键文件夹:
androidMain: 安卓特有的代码。iosMain: iOS 特有的代码。commonMain: 重点来了! 这里面的代码,安卓和 iOS 都能看见。commonTest: 公共测试代码。
3. 配置依赖
打开 build.gradle.kts 文件。你会看到类似这样的配置:
plugins {
kotlin("multiplatform") version "1.9.20" // 版本号建议用最新稳定版
id("com.android.library")
}
kotlin {
androidTarget()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
// 这里放共享逻辑需要的库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// 比如你想用 HTTP 客户端,可以用 ktor
implementation("io.ktor:ktor-client-core:2.3.5")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.5") // 安卓用 OkHttp 引擎
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.5") // iOS 用 Darwin (NSURLSession) 引擎
}
}
}
看懂了吗?这就是 KMP 的精髓:声明式依赖。你在 commonMain 里声明核心库,然后在 androidMain 和 iosMain 里分别指定具体的实现引擎。这样,你的业务代码完全不用关心底层是用 OkHttp 还是 NSURLSession,你只管调用 API。
第三步:编写你的第一个共享逻辑——天气查询
光说不练假把式。咱们写一个小功能:根据城市名称查询天气。为了简单起见,我们模拟一个 API 调用。
在 commonMain/kotlin/com/yourname/shared 目录下,新建一个文件 WeatherRepository.kt。
package com.yourname.shared
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// 定义数据模型,这个类安卓和 iOS 都能直接用
data class WeatherData(
val city: String,
val temperature: Double,
val condition: String
)
class WeatherRepository {
private val client = HttpClient()
suspend fun getWeather(cityName: String): WeatherData {
// 这里我们假装从网络获取数据,实际项目中你会用真实的 API
return withContext(Dispatchers.IO) {
try {
// 模拟网络请求延迟
kotlinx.coroutines.delay(500)
// 实际代码应该是:
// val response = client.get("https://api.weather.com/v1/current?city=$cityName")
// val json = response.body<JsonElement>()
// 为了演示,我们直接返回模拟数据
WeatherData(
city = cityName,
temperature = 25.5,
condition = "Sunny"
)
} catch (e: Exception) {
throw RuntimeException("Failed to fetch weather", e)
}
}
}
}
注意看,这段代码里没有安卓特有的 Activity,也没有 iOS 特有的 ViewController。它纯粹是逻辑。HttpClient 是 Ktor 的核心,我们在 build.gradle.kts 里已经配置好了不同平台的引擎,所以这段代码在两个平台上都能无缝运行。
第四步:让安卓“看见”共享代码
现在,逻辑写好了,安卓该怎么用呢?
你需要在你的安卓项目(通常是 app 模块)的 build.gradle.kts 中添加对 shared-core 的依赖。
dependencies {
implementation(project(":shared-core"))
// 其他安卓依赖...
}
然后,在你的安卓 Activity 或 ViewModel 中调用它:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val button = findViewById<Button>(R.id.btnGetWeather)
button.setOnClickListener {
lifecycleScope.launch {
val repository = WeatherRepository()
try {
val weather = repository.getWeather("Beijing")
Toast.makeText(this@MainActivity,
"北京天气: ${weather.temperature}度, ${weather.condition}",
Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "出错了", Toast.LENGTH_SHORT).show()
}
}
}
}
}
看,安卓代码极其干净。它不知道也不关心 WeatherRepository 是怎么获取数据的,它只知道调用这个方法就能拿到 WeatherData。
第五步:让 iOS“看见”共享代码
这是最关键的一步,也是很多新手卡壳的地方。iOS 项目不能直接“引用”一个 Gradle 项目,我们需要通过 CocoaPods 或者 Swift Package Manager 来集成。这里推荐 CocoaPods,因为它在混合开发中比较成熟。
1. 生成 XCFramework
在你的 shared-core 项目的根目录下,运行以下 Gradle 命令:
./gradlew assembleRelease
这会生成一个 .xcframework 文件,通常位于 build/XCFrameworks/release/ 目录下。这个文件包含了编译好的 iOS 二进制库。
2. 配置 iOS 项目
打开你的 iOS 项目(SwiftUI 或 UIKit 均可)。在项目根目录创建一个 Podfile 文件(如果没有的话)。
platform :ios, '14.0'
use_frameworks!
target 'YourIOSApp' do
# 指向你生成的 xcframework 路径
pod 'SharedCore', :path => '../shared-core/build/XCFrameworks/release'
end
然后在终端运行:
pod install
3. 在 Swift 中使用
打开 AppDelegate.swift 或你的某个 View 模型。你需要导入框架:
import SharedCore
class WeatherViewModel: ObservableObject {
@Published var weatherData: WeatherData?
@Published var errorMessage: String?
func fetchWeather(city: String) {
Task {
let repository = WeatherRepository()
do {
let data = try await repository.getWeather(cityName: city)
self.weatherData = data
} catch {
self.errorMessage = error.localizedDescription
}
}
}
}
哇,是不是不可思议?在 Swift 代码里,你可以直接实例化 WeatherRepository,调用 getWeather,甚至访问 WeatherData 的属性。Kotlin 的数据类会自动映射为 Swift 的结构体,属性名也自动驼峰转换。
第六步:处理平台特定代码——当逻辑需要“接地气”时
有时候,共享逻辑需要调用平台特有的功能。比如,你想获取设备的唯一 ID,或者读取相册照片。这时候,commonMain 里的代码就无能为力了,因为它不知道安卓有 Context,iOS 有 UIApplication。
KMP 提供了 expect/actual 机制来解决这个问题。
1. 在 commonMain 中声明接口
// commonMain/kotlin/com/yourname/shared/Platform.kt
package com.yourname.shared
// expect 关键字表示“期待”有一个实现,具体实现由平台提供
expect fun getPlatformInfo(): String
2. 在 androidMain 中提供实现
// androidMain/kotlin/com/yourname/shared/Platform.android.kt
package com.yourname.shared
import android.os.Build
import android.content.Context
import androidx.core.content.ContextCompat
// actual 关键字表示“实际”实现
actual fun getPlatformInfo(): String {
val context = ContextWrapper(ContextCompat.getSystemService(androidx.appcompat.app.AppCompatDelegate.currentApplication!!, Context::class.java)).baseContext
return "Android ${Build.VERSION.RELEASE}, Device: ${Build.MODEL}"
}
注:这里为了简化,假设你在非 UI 线程也能获取 Context,实际项目中可能需要更复杂的封装,比如通过 Hilt 注入或单例持有 Application Context。
3. 在 iosMain 中提供实现
// iosMain/kotlin/com/yourname/shared/Platform.ios.kt
package com.yourname.shared
import platform.UIKit.UIDevice
import platform.UIKit.UIApplication
actual fun getPlatformInfo(): String {
val device = UIDevice.currentDevice
return "iOS ${device.systemVersion}, Model: ${device.model}"
}
现在,回到 WeatherRepository,你可以这样调用:
suspend fun getWeatherWithDeviceInfo(cityName: String): WeatherData {
val info = getPlatformInfo() // 调用 expect/actual 函数
println("Running on: $info")
// ... 其余逻辑
}
这样,你就完美地实现了“通用逻辑 + 平台适配”的模式。
第七步:调试与常见问题排查——别怕报错
刚开始接触 KMP,报错是家常便饭。尤其是类型转换和协程调度方面。
问题 1:找不到符号?
检查你的 build.gradle.kts,确保 commonMain.dependencies 里的库版本一致。Ktor 的版本必须严格匹配,否则会出现 NoClassDefFoundError。
问题 2:iOS 编译失败?
很多时候是因为 CocoaPods 缓存问题。尝试删除 Podfile.lock 和 Pods 文件夹,重新运行 pod install。另外,确保你的 Xcode 命令行工具指向正确:sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer。
问题 3:协程挂起点问题?
在 commonMain 中,尽量避免使用平台特定的线程调度器。使用 Dispatchers.Default 或 Dispatchers.IO 时,确保它们在两端都有对应的实现。KMP 默认提供了这些,但如果自定义了调度器,需要通过 expect/actual 暴露。
第八步:进阶技巧——如何教小朋友理解这个概念?
如果你要给家里的小朋友解释什么是 Kotlin Multiplatform,你可以这么说:
“宝贝,你看,我们有一块乐高积木(这是你的业务逻辑代码)。以前,如果你想把积木放在红色的桌子上(安卓手机),你需要专门做一套红色的底座;如果想放在蓝色的桌子上(苹果手机),又得做一套蓝色的底座。现在,我们发明了一种‘万能接口’(KMP),这块乐高积木底下自带了一个转换器。不管桌子是什么颜色,只要插上去,就能稳稳地站着,而且还能玩起来!”
这个比喻其实涵盖了 KMP 的核心价值:解耦。UI 是桌子,逻辑是积木,KMP 是转换器。
第九步:实战演练——构建一个简单的笔记应用骨架
让我们把这些碎片拼起来。假设我们要做一个极简的笔记应用。
1. 数据层 (commonMain)
data class Note(val id: Long, val title: String, val content: String)
interface NoteStorage {
suspend fun saveNote(note: Note)
suspend fun getAllNotes(): List<Note>
}
// 使用 expect/actual 来抽象存储实现
expect fun createDefaultStorage(): NoteStorage
2. 安卓实现 (androidMain)
actual fun createDefaultStorage(): NoteStorage {
return AndroidRoomStorage() // 内部使用 Room 数据库
}
3. iOS 实现 (iosMain)
actual fun createDefaultStorage(): NoteStorage {
return IOSCoreDataStorage() // 内部使用 CoreData 或 SQLite
}
4. 业务逻辑 (commonMain)
class NoteManager(private val storage: NoteStorage) {
suspend fun addNewNote(title: String, content: String) {
val newNote = Note(System.currentTimeMillis(), title, content)
storage.saveNote(newNote)
}
suspend fun loadNotes(): List<Note> {
return storage.getAllNotes()
}
}
5. 安卓 UI (androidMain)
class NoteViewModel : ViewModel() {
private val manager = NoteManager(createDefaultStorage())
var notes by mutableStateOf<List<Note>>(emptyList())
private set
fun loadNotes() {
viewModelScope.launch {
notes = manager.loadNotes()
}
}
}
6. iOS UI (iosMain)
class NoteViewModel: ObservableObject {
@Published var notes: [Note] = []
private let manager: NoteManager
init() {
// 注意:这里需要桥接 Kotlin 对象到 Swift
// 实际上,KMP 的 expect/actual 生成的 Swift 接口允许你直接调用
let storage = createDefaultStorage()
manager = NoteManager(storage: storage)
}
func loadNotes() {
Task {
let loaded = try await manager.loadNotes()
self.notes = loaded
}
}
}
通过这个骨架,你会发现,无论是安卓还是 iOS,NoteManager 的逻辑是完全一样的。你只需要专注于如何用 Compose 画列表,或者用 SwiftUI 画列表。
第十步:未来展望——KMP 不仅仅是跨平台
很多人认为 KMP 只是为了省代码量。其实不然,它更是为了团队融合。
在一个大型公司里,安卓团队和 iOS 团队往往井水不犯河水。但有了 KMP,你可以建立一个“共享核心团队”,他们只维护 commonMain 里的逻辑。这个团队的成员可以是全栈工程师,他们既懂 Kotlin 也懂 Swift。
随着时间推移,你会发现:
- Bug 率下降:因为逻辑只有一份,不会出现“安卓端显示正常,iOS 端崩溃”的情况。
- 开发速度提升:新功能上线,只需修改一处,两端同时受益。
- 技术债减少:不再需要维护两套几乎相同的网络层、加密层、数据解析层。
当然,KMP 也有局限性。比如,它的社区生态相比 Flutter 或 React Native 还要年轻,一些第三方库可能还没有官方支持。这时候,你就需要自己写 expect/actual 适配器,或者等待社区贡献。但这正是锻炼你底层能力的机会。
结语:迈出第一步的勇气
写到这里,你可能觉得:“听起来挺复杂,我还是先从 Hello World 开始吧。”
没错,别被长篇大论吓倒。Kotlin Multiplatform 的学习曲线是呈 J 型分布的:前期有点陡峭,因为你要同时理解 Gradle、CocoaPods、Swift 和 Kotlin;但一旦你跨过了那个门槛,后面就是平直的上升通道,甚至会有下坡的轻松感。
我建议你今天就做一件事:
- 安装 IntelliJ IDEA。
- 创建一个空的 KMP Library 项目。
- 在
commonMain里写一个简单的加法函数。 - 在
androidMain和iosMain里分别打印结果。
当你看到控制台同时输出安卓和 iOS 的结果时,你会感受到那种“掌控两端”的快感。
记住,编程不是为了背诵语法,而是为了解决问题。KMP 解决的是“重复劳动”和“数据一致性”的问题。掌握了它,你就不仅仅是一个安卓开发者或 iOS 开发者,你是一个真正的移动应用架构师。
加油,未来的全栈大神!如果在路上遇到任何坑,记得回来看看这篇文章,或者直接在评论区问我。咱们一起把这条路走通。
