2. KMP项目工程结构搭建:模块划分、共享代码、平台特定代码
本章聚焦KMP(Kotlin Multiplatform)项目的物理模块划分规则,以及如何在Gradle多模块体系中组织共享代码与平台特定代码。理解这一结构是后续实现Compose状态提升的基础——只有正确定义commonMain、androidMain、iosMain等source set,才能让业务逻辑与平台UI层无缝配合。
2.1 原理:KMP的source set体系
KMP利用Gradle的多源集(Source Set)机制为不同目标平台提供专用目录。每个模块内至少包含以下标准源集:
| 源集名称 | 用途 | 典型目录 |
|---|---|---|
commonMain | 平台无关的业务逻辑、数据模型、接口声明 | src/commonMain/kotlin |
commonTest | 共享测试代码 | src/commonTest/kotlin |
androidMain | Android平台具体实现(如Android API调用、依赖注入) | src/androidMain/kotlin |
iosMain | iOS平台具体实现(如UIKit交互、NSUserDefaults) | src/iosMain/kotlin |
desktopMain(可选) | 桌面平台(JVM) | src/desktopMain/kotlin |
实际项目中通常将UI相关的Compose代码也放入commonMain(Compose Multiplatform)。平台特定入口(如Activity、View Controller)则分别放置在androidMain和iosMain。这种划分严格遵循“expect/actual”机制——在commonMain中声明接口或类(expect),在各平台源集中提供具体实现(actual)。
核心原则:业务逻辑、网络请求、数据库操作、状态管理应全部置于commonMain;系统专属功能(传感器、通知、文件读写)放在平台源集,并通过expect/actual暴露给共享层。
2.2 架构总览
下图展示一个典型KMP + Compose Multiplatform项目的模块依赖关系:
2.3 实现流程:手动搭建KMP项目结构
推荐使用JetBrains插件“Kotlin Multiplatform Wizard”生成初始模板,然后按需调整。若需手动创建,请遵循以下步骤:
- 创建根项目:使用Gradle Kotlin DSL,指定Kotlin与Compose插件版本。
- 定义模块:在
settings.gradle.kts中包含shared、androidApp、iosApp。 - 配置shared模块:声明目标平台(androidNativeX64, iosArm64, iosSimulatorArm64等),添加Compose Multiplatform依赖。
- 配置Android模块:依赖shared模块,添加Android Compose和Kotlin Android插件。
- 配置iOS模块:创建Xcode项目,引入shared的framework产物,桥接Compose UI。
以下是shared/build.gradle.kts的核心配置片段:
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
}
kotlin {
androidTarget()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { target ->
target.binaries.framework {
baseName = "shared"
isStatic = true
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("io.ktor:ktor-client-core:2.3.12")
}
}
val androidMain by getting {
dependencies {
implementation(compose.preview)
implementation("androidx.activity:activity-compose:1.9.2")
}
}
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
// iOS平台特定依赖(如Ktor iOS引擎)
implementation("io.ktor:ktor-client-darwin:2.3.12")
}
}
}
}
经验值:若需在iOS上使用Compose,务必在iosMain中加入
compose.ui依赖(Compose插件会自动处理)。framework导出时,注意选择isStatic = true以减少启动时间。
2.4 平台特定代码:expect/actual实践
在commonMain中声明期望接口,例如平台剪贴板操作:
// commonMain
expect class ClipboardManager {
fun copy(text: String)
fun paste(): String?
}
Android实现:
// androidMain
actual class ClipboardManager(private val context: Context) {
private val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
actual fun copy(text: String) {
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("label", text))
}
actual fun paste(): String? {
val clip = clipboard.primaryClip
return if (clip != null && clip.itemCount > 0) clip.getItemAt(0).text.toString() else null
}
}
iOS实现:
// iosMain
actual class ClipboardManager {
private val pasteboard = UIPasteboard.generalPasteboard
actual fun copy(text: String) {
pasteboard.string = text
}
actual fun paste(): String? = pasteboard.string
}
2.5 调试与运行
- Android:直接运行
androidApp任务,如同普通Android项目。 - iOS:先执行
linkDebugFrameworkIosArm64(或对应架构任务),将生成的shared.framework放入Xcode项目,然后在Xcode中运行iOS target。 - 跨平台调试:使用Android Studio或IntelliJ IDEA的KMP调试器,断点可同时进入commonMain和平台代码。注意:iOS调试需在macOS上通过Xcode进行。
2.6 常见错误与解决方案
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| 编译报错“Expecting actual declaration in module xxx” | 平台源集未正确关联,或actual函数签名不一致 | 检查iosMain是否通过dependsOn(commonMain)连接;确保函数名、参数、返回类型完全匹配 |
| iOS链接时找不到shared框架 | 框架名称或路径配置错误 | 在build.gradle.kts中设置baseName = "shared",并在Xcode的Framework Search Paths中添加$(SRCROOT)/../shared/build/bin/iosArm64/debugFramework |
| Compose预览在Android中不显示 | 缺少compose.preview依赖 | 在androidMain依赖中添加implementation(compose.preview) |
| Gradle同步超时 | Kotlin插件版本与Compose版本不兼容 | 使用BOM统一版本:implementation(platform("org.jetbrains.compose:compose-bom:2024.10.01")) |
2.7 性能与稳定性优化
- 模块层次不宜过深:尽量保持共享模块扁平化,避免多层依赖导致增量编译变慢。
- 谨慎使用expect/actual:每增加一对expect/actual都会增加跨平台编译的复杂度。优先用接口+平台注入替代直接的expect声明。
- iOS Framework静态链接:始终设置
isStatic = true,减少动态链接的启动开销和版本冲突。 - 开启编译缓存:在
gradle.properties中添加:kotlin.mpp.stability.nowarn=true kotlin.mpp.enableCInteropCommonization=true org.gradle.caching=true
警告:切勿将平台特定代码(如Android的
android.util.Log)直接写在commonMain中,即使通过条件编译也是反模式。务必使用expect/actual或依赖注入隔离平台依赖。