2. 搭建第一个跨平台 Compose 工程
上一节我们理解了 Compose Multiplatform 的定位,这一节要把概念落成一个真正能跑起来的工程。目标很明确:一个仓库、一套 Gradle 构建、一份共享 UI 代码,同时产出 Android、iOS、Desktop、Web 四个平台的可运行产物。关键不在于“点几下向导”,而在于理解工程骨架里每个文件为什么存在、源集如何划分、各平台入口如何被组织起来。
一、工程骨架与插件体系
Compose Multiplatform 工程本质上是一个 Kotlin Multiplatform(KMP)工程,只是额外引入了 Compose 相关的 Gradle 插件。一个典型结构如下:
composeMultiplatformDemo/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml
├── composeApp/
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/kotlin/ # 共享 UI 与逻辑
│ ├── androidMain/kotlin/ # Android 入口
│ ├── iosMain/kotlin/ # iOS 入口
│ ├── desktopMain/kotlin/ # Desktop 入口
│ ├── wasmJsMain/kotlin/ # Web 入口
│ └── commonMain/composeResources/
└── iosApp/ # Xcode 工程,调用共享框架
插件是这套骨架的“发动机”。在 composeApp/build.gradle.kts 中,通常需要声明四类插件:kotlin("multiplatform") 负责多平台编译与源集管理;org.jetbrains.compose 提供 Compose 的依赖与打包能力;org.jetbrains.kotlin.plugin.compose 是 Kotlin 2.0 之后独立的 Compose 编译器插件,负责把 @Composable 函数编译成 UI 代码;com.android.application 则让 Android 目标能产出 APK。版本建议统一放在 libs.versions.toml 里,避免各模块版本漂移。
org.jetbrains.kotlin.plugin.compose,否则 @Composable 会编译失败。这是新手最常见的报错来源之一。二、源集划分:共享什么,隔离什么
KMP 的核心思想是“按需共享”。commonMain 存放所有平台都能用的代码,包括 Compose UI、状态管理、数据模型、网络请求逻辑等。平台专属代码放在各自的源集里,例如 Android 的 MainActivity、iOS 的 MainViewController、Desktop 的 main 函数、Web 的 main 函数。它们只做一件事:把共享的 App() 组合函数挂到平台入口上。
当某个能力各平台实现不同(如读取系统时间、访问文件、调用平台 API),就在 commonMain 里用 expect 声明,在各平台源集里用 actual 实现。这样共享代码只依赖抽象接口,编译时由各平台提供具体实现。源集之间还存在继承关系:iosMain 可以同时被 iosX64、iosArm64、iosSimulatorArm64 复用,避免为每个 iOS 架构重复写入口代码。
三、Gradle 配置的关键片段
在 composeApp/build.gradle.kts 中,kotlin { } 块负责声明目标平台与源集依赖。下面是一份可直接参考的骨架(版本号以实际为准):
kotlin {
androidTarget()
jvm("desktop")
iosX64(); iosArm64(); iosSimulatorArm64()
@OptIn(ExperimentalWasmDsl::class)
wasmJs { browser() }
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.resources)
}
}
val desktopMain by getting {
dependencies { implementation(compose.desktop.currentOs) }
}
}
}
compose.desktop {
application {
mainClass = "MainKt"
}
}
注意 jvm("desktop") 给 JVM 目标起了别名,源集名随之变成 desktopMain,入口类名要与 mainClass 对应。Web 目标使用 wasmJs 而非旧的 js,因为 Compose for Web 已全面转向 Wasm 运行时,性能与兼容性更好。
四、四个平台的入口写法
共享的 App() 是唯一 UI 来源,各平台入口只是“挂载器”。Android 在 MainActivity 中调用 setContent { App() };Desktop 在 main() 中用 application { Window(onCloseRequest = ::exitApplication) { App() } };iOS 通过 ComposeUIViewController { App() } 返回一个 UIViewController,再由 Swift 侧嵌入;Web 则用 CanvasBasedWindow("App") { App() } 把 Compose 渲染到页面 canvas 上。
四个入口代码加起来通常不超过 40 行,这正是 KMP 的价值:平台差异被压缩到最小,业务与 UI 全部共享。
五、运行与验证
配置完成后,用 Gradle 任务逐个验证目标是否可用:
./gradlew :composeApp:assembleDebug # Android APK
./gradlew :composeApp:run # Desktop 窗口
./gradlew :composeApp:wasmJsBrowserRun # Web 浏览器
# iOS:用 Xcode 打开 iosApp/iosApp.xcodeproj 运行
判断成功的标准很直接:Android 安装后能看到共享 UI;Desktop 弹出窗口;浏览器打开本地地址后页面渲染出同样的界面;iOS 模拟器启动后显示相同内容。如果某个平台报错,优先检查三处:插件是否声明齐全、源集目录名是否与目标名一致、expect/actual 是否在所有平台都有对应实现。只要这三点对齐,四端同源运行就是水到渠成的事。