关于 kmp-boundaries
面向 Kotlin Multiplatform 的边界设计技能,说明如何划分共享代码与平台实现。重点涵盖按能力拆分接口、由 Activity 持有的 Android UI 绑定,以及 AGP 9 KMP 库的结构限制。
主要功能
- 01语义化的 commonMain 接口
- 02按能力划分平台边界
- 03Activity 作用域的 Android UI 绑定
- 04AGP 9 KMP 库限制
- 05iOS–Swift 互操作参考
适用场景
- 01设计平台能力的共享 API
- 02避免 Android 绑定中的 Activity 生命周期泄漏
- 03将 Android 应用迁移为 AGP 9 KMP 库
- 04为 Swift 调用准备 Kotlin API
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rcosteira79/ android-skills --skill kmp-boundaries安装后在 Claude Code 中输入/kmp-boundaries确认已加载。
附带文件references/ios-interop.md
详细介绍
Kotlin 多平台边界设计
KMP 边界的核心规则:
- 让
commonMain保持语义化 — 描述产品需要什么,而非 Android/iOS 的机制:使用currentRegion(),而不是currentRegionFromAndroidLocale(context)。 - 按能力拆分 — 将
Clipboard、ShareSheet、Haptics、Biometrics设计为独立接口,而不是一个包揽一切的Platform对象。 - 让 actual 保持精简 — 它们负责转换,不负责决策;actual 内的业务
if/when应放在 common 中,并用 fake 测试。 - 只要需要 fake、DI、生命周期或运行时选择,就优先使用 common
interface+ 每个平台的绑定,而不是expect class。 - 仅当两个平台确实共享同一个 actual 时,才引入中间源集(
skikoMain、appleMain)。
下面会详细介绍两个边界:由 Activity 持有的平台 UI 边界,以及 AGP-9 KMP 库的约束。
相关内容: android-skills:kmp-ktor(网络边界)、compose/references/multiplatform.md(Compose-MP 机制)、android-skills:kotlin-coroutines(作用域所有权)。对于 iOS↔Swift 桥接——Kotlin→Swift 命名、类型宽度(Int 为 32 位)、SKIE suspend→async / Flow→AsyncSequence、密封类完备性、SwiftUI 嵌入——编写 iOS 端 actual 时,请加载 references/ios-interop.md。
平台 UI 绑定由 Activity 持有,而非 Context 持有
Android 边界中最常见的错误是:将 applicationContext / LocalContext.current 传入实际上需要 Activity 的绑定,然后用 Intent.FLAG_ACTIVITY_NEW_TASK 来掩盖生命周期缺口。这个标志是个警示信号——它掩盖了这是一项前台 UI 操作的事实。应持有 Activity。
// commonMain — semantic interface; DOCUMENT what `suspend` means
interface ShareSheet {
/** Launches the system share sheet. Returns when the sheet is PRESENTED — not when the user
* completes or cancels. (Otherwise callers write incorrect retry/confirmation logic.) */
suspend fun shareText(text: String)
}
// androidMain — thin: build the intent and launch it. Activity-owned.
class AndroidShareSheet(private val activity: Activity) : ShareSheet {
override suspend fun shareText(text: String) {
activity.startActivity(Intent.createChooser(
Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, text), null,
))
}
}
不要在整个应用范围内注入 Activity(它由框架创建,并受生命周期约束)——应在 Android 应用模块的 Activity 作用域中构建绑定(Hilt 使用 @InstallIn(ActivityComponent::class),其中 Activity 是默认绑定;Koin 使用 scoped)。commonMain 只会看到接口;Activity 不会离开应用模块。如果生命周期更长的(应用作用域)对象需要它,应通过感知生命周期的提供方持有它(在 onResume 中设置,在 onPause 中清除),以免已销毁的 Activity 发生泄漏。
AGP 9 KMP 库的限制(结构性限制——决定共享代码中可以包含哪些内容)
AGP 9 将 KMP 模块 Android 端的 com.android.library 替换为 com.android.kotlin.multiplatform.library,并彻底拒绝 com.android.application + kotlin.multiplatform 的组合。新插件强制采用单变体架构:
- 无法使用
BuildConfig——编译时常量应来自 BuildKonfig 或注入的AppConfiguration接口。设计commonMainAPI 时,不要假设存在BuildConfig.X。 - 没有构建变体——特定变体的依赖项、资源和签名应放在应用模块中;debug/release 的区别应作为运行时配置值注入共享代码,而不是在 KMP 模块内部拆分构建变体。
- 没有 NDK / JNI——将原生代码(C/C++)提取到单独的
com.android.library模块中,并通过 KMP 模块使用的公共接口进行封装。 - 必须显式启用 Compose-MP 资源——在
kotlin { android { … } }内添加androidResources { enable = true },否则 Android 上的Res.string.*/Res.drawable.*会在运行时崩溃(构建仍会成功——很容易忽略这一点)。 - 必须迁移 Consumer ProGuard 规则——旧
android {}块中的consumerProguardFiles("rules.pro")会被静默丢弃;请在新 DSL 中使用consumerProguardFiles.add(file("rules.pro"))。 - KMP 模块不能同时使用
com.android.application——Android 入口点(MainActivity、Application 类、启动器清单、applicationId/targetSdk/versionCode/versionName)需要移到依赖共享库的单独androidApp模块中。MainActivity、应用级 Hilt 配置和导航主机接线都要从共享的androidMain中移出。 - kapt 与 AGP 9 内置 Kotlin 不兼容——将注解处理器迁移到 KSP(2.3.1+),或对于没有 KSP 对应版本的处理器,改用
com.android.legacy-kapt。
| 关注点 | AGP 9 之前(单体式) | AGP 9 KMP 库 |
|---|---|---|
MainActivity、Application 类、启动器清单 | 共享模块的 androidMain | 单独的 androidApp 模块 |
applicationId、versionCode、targetSdk | 共享模块的 android {} | 仅限 androidApp |
| 编译时常量(环境、标志) | BuildConfig 字段 | common 中的 BuildKonfig,或运行时 DI |
| NDK / JNI 原生代码 | androidMain(任意模块) | 单独的 com.android.library,通过公共接口封装 |
迁移现有项目时,请参阅 JetBrains 的 kotlin-tooling-agp9-migration 技能,了解完整流程。