关于 kotlin-multiplatform-expect-actual
面向 Kotlin Multiplatform 开发者的边界设计技能,说明如何划分共享代码与平台实现。它指导开发者在 expect/actual 声明和接口之间做选择,并保持 common API 语义清晰且便于测试。
主要功能
- 01选择 expect/actual 或接口
- 02设计语义化的 common API
- 03将平台机制留在 actual 中
- 04Compose 平台叶节点指导
- 05跨源集验证实践
适用场景
- 01为平台能力设计共享 API
- 02为 common 代码添加可伪造依赖
- 03将平台 UI 与共享 Compose 屏幕分离
- 04检查 common 代码是否泄漏平台类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill kotlin-multiplatform-expect-actual安装后在 Claude Code 中输入/kotlin-multiplatform-expect-actual确认已加载。
详细介绍
Kotlin Multiplatform:expect/actual 边界
核心原则
让 common API 语义明确且稳定。将平台机制封装在简洁的 expect/actual 声明或接口后面,并将 Android/iOS/Desktop 的细节排除在 commonMain 之外。
边界流程
- 用通用术语描述产品能力:分享文本、读取剪贴板、请求触觉反馈、解析当前地区。
- 检查 common 调用方是否需要伪实现、注入依赖、管理生命周期,或在运行时选择实现。
- 从下表中选择最小的边界。
- 确保 common 签名不包含平台类型和平台术语。
- 将业务分支放在 common 代码中;将 actual/平台绑定作为转换层。
- 尽可能通过编译所有受影响的源集,并使用伪实现测试 common 代码来验证。
选择边界
| 情况 | 优先选择 |
|---|---|
| 简单的编译时平台特化 | expect/actual 函数、值、类型别名或叶级可组合项 |
| 实现需要注入依赖、管理生命周期、运行时选择或测试伪实现 | common 接口加平台绑定 |
| UI 大部分可共享,仅一个叶级部分不同 | 由 common 可组合项调用 expect 叶级项 |
| 整个屏幕因平台而异 | 在 common 导航契约后设置独立的平台屏幕 |
| 只有常量/资源不同 | 提供语义值的 common API,以及各平台的 actual 值 |
让 common API 具有语义性
编写 common API 时,应让调用方描述意图,而非平台机制:
// GOOD: common API is semantic
expect fun currentRegion(): Region
// BAD: common API leaks Android implementation
expect fun currentRegionFromAndroidLocale(context: Context): Region
Android actual 可以使用 Locale API。iOS actual 可以使用 Foundation API。common 调用方不应了解这些细节。
保持 actual 精简
Actual 实现应将语义 API 转换为平台调用。如果操作需要 Activity、视图控制器、生命周期所有者、DI 或伪实现,请停止使用 expect class,改用由平台代码提供的接口:
// commonMain
interface ShareSheet {
suspend fun shareText(text: String)
}
// androidMain
class AndroidShareSheet(
private val activity: Activity,
) : ShareSheet {
override suspend fun shareText(text: String) {
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, text)
activity.startActivity(Intent.createChooser(intent, null))
}
}
Android 实现明确由 Activity 管理。通用的 Context 往往会掩盖 UI 生命周期要求。应明确 suspend 的含义:对于许多平台 UI 操作,它表示“已启动分享表单”,而非“用户已完成分享”。
如果 actual 开始累积业务规则,就将这些规则移回 common 代码,只在 actual 中保留平台转换。
测试或 DI 很重要时优先使用接口
简单的编译时平台 API 使用 expect/actual。如果 common 代码需要伪实现、多个实现、运行时选择或生命周期管理,则使用接口:
interface Clipboard {
suspend fun setText(text: String)
}
平台模块将 Clipboard 绑定到 Android/iOS 实现。common 测试使用伪实现。
Compose 专项指导
共享 UI 延伸到平台叶级部分时:
- 将平台专属的可组合项保持在叶节点。
- 对每个会发出 UI 的预期可组合项传递
Modifier。 - 拒绝在
commonMain签名中使用平台类型(Context、Activity、Android 资源 ID、Uri、Bundle、UIViewController、NSBundle、平台权限枚举等)。 - 在平台 actual 中隐藏原生视图生命周期,并使用正确的互操作容器(
AndroidView、UIKitView等)。 - 不要直接从可组合项主体启动平台操作。在 actual 可组合项中使用
remember、LaunchedEffect、DisposableEffect和稳定键,就像在 common Compose 代码中一样。 - 尽可能使用伪平台服务预览/测试 common 普通 UI 可组合项。
常见错误
| 错误 | 修正方法 |
|---|---|
commonMain API 暴露 Android/iOS 类型 | 替换为语义化的通用类型 |
expect 函数包含仅适用于某个平台的参数 | 将这些细节移到 actual 中 |
| 各 actual 中重复编写业务分支 | 将业务规则移到通用代码中 |
使用一个庞大的 Platform expect 对象 | 按功能拆分:Clipboard、ShareSheet、Haptics |
| 平台 UI 泄漏到组件树的较高层 | 将平台专属的 Composable 下沉到叶节点 |
| 通用测试缺少可伪造的边界 | 使用接口,而不是直接调用 expect |
| 更改后只有一个目标能够编译 | 完成前编译所有受影响的源集 |
评审中的危险信号
- 通用代码导入平台包。
- actual 实现了解产品状态、导航决策或领域规则。
- 平台 API 名称出现在通用函数名中。
- 添加第三个平台时需要修改通用调用方。
- 仅为验证通用业务行为,测试就需要 Android/iOS 运行时。
相关内容(Compose / 共享 UI)
在此技能中专注于平台边界;像处理其他 Compose 目标一样接入共享 UI:
kotlin-control-flow— 使用when、保护条件、穷尽性检查和智能转换,明确通用代码中的业务分支。compose-state-hoisting— 共享的纯 UI 可组合项与状态持有者接线。compose-side-effects— 实际可组合项中的副作用键和清理(LaunchedEffect、DisposableEffect等)。compose-modifier-and-layout-style和compose-slot-api-pattern— 可复用的共享 Compose API(修饰符、插槽)。