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 里,避免各模块版本漂移。

Compose 编译器插件从 Kotlin 2.0 起与 Kotlin 版本解耦,必须显式声明 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 架构重复写入口代码。

Compose Multiplatform 工程结构与源集依赖 commonMain App() · UI · 状态 · 数据模型 expect 声明 · composeResources androidMain MainActivity setContent { App() } iosMain MainViewController ComposeUIViewController desktopMain main() application { Window } wasmJsMain main() CanvasBasedWindow APK / AAB Android 设备 .framework Xcode 链接 JVM 可执行包 Windows/macOS/Linux Wasm + JS 浏览器加载 commonMain 只依赖抽象接口,各平台源集提供 actual 实现与入口

三、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 是否在所有平台都有对应实现。只要这三点对齐,四端同源运行就是水到渠成的事。