说了这么久,KMP 的工程默认结构终于还是官宣了,简单来说就是,以前一个 composeApp 模块既可以当共享库,又可以当 Android/Desktop/Web App 入口,但是现在默认会拆成 shared 共享模块 + 各平台独立 App 模块,比如 androidApp、desktopApp、webApp。
在以前的老结构里,大多数项目都有一个单独的 composeApp Gradle 模块,它既是 Kotlin Multiplatform 共享代码模块,又包含 Android App 入口、Desktop App 入口、Web App 入口,还塞了各个平台的打包配置:

如果在项目级别看,老项目界沟大概如下所示,JetBrains 认为这导致一个大问题,因为你很难一眼看出哪些配置是共享库的,哪些配置是具体 App 的 ,所以旧的 composeApp 模块承担了太多功能。

而在新的默认结构里通过一个 shared 模块来解决问题 , shared 只负责一件事:放共享代码 ,也就是 shared 只能放共享的能力,而对于单独的 App 模块,放在例如 androidApp 、 desktopApp 和 webApp 里面:

也就是 shared 只包含共享代码的 KMP library,而每个平台的 runnable application 一般放到独立模块里,在工程试图下大概就是如下所示效果:

新的架构看起来能力更简洁更独立,当然这里面还有一个重要原因就是 Android Gradle Plugin 9.0 。
AGP 9.0 要求 Android 应用入口和共享代码放在不同模块里,因为它不再支持在 multiplatform module 里应用 Android application Gradle plugin。
所以另一个原因就是 JetBrains "被动"跟进 ,之前很多 KMP 项目会在同一个模块里同时用:
org.jetbrains.kotlin.multiplatform
com.android.application
也就是 composeApp 既是 KMP 模块,又是 Android App 模块,但是到了 AGP 9 就不兼容了,需要用
dart
com.android.kotlin.multiplatform.library
并且以前的 com.android.library、com.android.application 页不能和 KMP plugin 放在同一个模块里混用,所以这次 KMP 我感觉更像是为了 AGP 9 的新规则进行适配。
当然,JetBrains 还提到了很重要的一点,如果 iOS 用原生 SwiftUI,结构还需要再加一层,比如你不是所有平台都共享 Compose UI :
- Android / Desktop / Web 用 Compose Multiplatform
- iOS 用 SwiftUI
那 JetBrains 推荐结构是:

这里的区别是 sharedLogic 放所有平台都要用的业务逻辑,不依赖 Compose,而 sharedUI 放只有使用 Compose Multiplatform 的平台才需要的共享 UI。
也就是如果某个平台用原生 UI,比如 iOS 用 SwiftUI,那么新结构下 ``shared` 就需要拆成两个共享模块:
sharedLogic被所有 App 消费,不带 Compose 依赖sharedUI只给使用 Compose Multiplatform UI 的平台用
如果包含服务器场景,那么新的结构会添加一个 server 模块,同时额外增加 core模块共享代码 :

另外官方也说了,以前 Gradle 版 KMP 模板 和 Amper 版 KMP 模板长得不一样,因为两套构建系统的抽象不同 ,这次新 KMP 默认结构,其实页是在向 Amper 的结构靠拢 ,这个我们在之前的 《JetBrains Amper 0.10 ,期待它未来替代 Gradle》 也大致聊过:

未来 KMP 大概率会直接采用 Amper 管理。
那么老项目需要强制迁移吗?首先通过引导创建的新建项目默认就会使用新架构了,已有老项目不会强制,但是如果你要上 AGP 9,那么 Android 相关迁移是强制的了,所以如果你要升 AGP 9 ,还是需要使用新架构。
实际上迁移并不复杂,复杂的是你的那些自动易操作,从目前官方的路径看,迁移一般主要有:
Android
- 新建
androidApp模块,在settings.gradle.kts加:
php
include(":androidApp")
androidApp/build.gradle.kts使用 Android App 相关插件:
groovy
plugins {
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
androidApp依赖共享模块:
groovy
kotlin {
dependencies {
implementation(projects.composeApp) // 或 projects.shared
implementation(libs.androidx.activity.compose)
}
}
-
把原来
composeApp里的android {}配置复制到androidApp。 -
把
composeApp/src/androidMain移到:
css
androidApp/src/main
这里需要注意
MainActivity.kt这种入口代码搬走,expect/actual` 相关代码不能乱搬,一般还是应该留在共享模块 source set 里。
shared
如果原来的 composeApp 继续作为 shared 模块用,就要把它从 Android App 改成 Android-KMP library :
groovy
// 旧
alias(libs.plugins.androidApplication)
// 新
alias(libs.plugins.androidMultiplatformLibrary)
在版本目录加:
groovy
[plugins]
androidMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
同时 shared 模块里不要再用根级 android {},而是在 kotlin {} 里用:
groovy
kotlin {
androidLibrary {
namespace = "your.namespace"
compileSdk = libs.versions.android.compileSdk.get().toInt()
compilerOptions {
jvmTarget = JvmTarget.JVM_11
}
androidResources {
enable = true
}
}
}
最后删掉 shared 模块里原来的:
android {}
androidTarget {}
androidMain.dependencies {}
applicationId / versionCode / versionName
Desktop
新建 desktopApp ,然后在 settings.gradle.kts 增加:
php
include(":desktopApp")
同时 desktopApp 使用:
scss
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
把原来的 composeApp/src/jvmMain/kotlin 放到 desktopApp/src/main/kotlin,然后把原来 compose.desktop {} 也从 composeApp 搬到 desktopApp,迁移成功后 composeApp 里删除:
scss
jvm() target
jvmMain.dependencies
compose.desktop {}
composeApp/src/jvmMain
注意 package 和
compose.desktop { application { mainClass = ... } }对齐,否则 Desktop 启动入口会找不到。
Web
同理新建 webApp ,然后 settings.gradle.kts 添加:
php
include(":webApp")
之后 webApp 使用:
scss
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
一样是把原来的 JS/Wasm target 搬到 webApp:
scss
kotlin {
js {
browser()
binaries.executable()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
}
把 composeApp/src/webMain 移动到 webApp/src/webMain , 然后改 index.html 里的脚本名:
rust
composeApp.js -> webApp.js
迁移成功后,composeApp 里删除:
bash
webMain.dependencies
js {}
wasmJs {}
composeApp/src/webMain
sharedLogic / sharedUI
如果所有平台都用 Compose UI,那么其实一个 shared 就够 ,而前面我们说的,如果 iOS 用 SwiftUI,或者某个平台不用 Compose UI,才需要拆成 sharedLogic + sharedUI :
sharedLogic放纯业务逻辑,比如 model、network、repository、validation、时间计算等,不依赖 Compose
0 sharedUI 放 Compose Multiplatform UI、resources、App() composable 等,可以依赖 sharedLogic
比如官方 Demo 里
currentTimeAt()这种纯函数就挪到sharedLogic,但依赖DrawableResource的Country就不能放进sharedLogic,因为它已经依赖 Compose resources。
iOS
iOS 在这里比较特殊,它不是一个普通 Gradle App 模块,而是 Xcode 工程消费 Kotlin 产物,所以如果共享模块换名了,比如从 composeApp 变成 shared 或 sharedUI,就要改 Xcode 里的 Gradle embed 脚本:
ruby
./gradlew :shared:embedAndSignAppleFrameworkForXcode
或者:
ruby
./gradlew :sharedUI:embedAndSignAppleFrameworkForXcode
但 Swift 里的 import 名不一定跟 Gradle 模块名一致,它取决于 Kotlin/Native framework 的:
ini
baseName = "sharedUI"
所以 Swift import 对应的是 framework
baseName,不是 Gradle module 名。
最后
到这里,应该都能理解,新结构的好处显而易见了,整体更清晰、职责更单一、后续模块化更自然,但是就是项目管理成本提高了,不过对中大型项目来说更好,对中小型项目来说就有些浪费表情的感觉。