kotlin-multiplatform-expect-actual
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKotlin Multiplatform: expect/actual boundaries
Kotlin Multiplatform:expect/actual边界
Core principle
核心原则
Keep common APIs semantic and stable. Put platform mechanics behind small / declarations or interfaces, and keep Android/iOS/Desktop details out of .
expectactualcommonMain保持通用API的语义化与稳定性。将平台机制封装在小型/声明或接口之后,避免将Android/iOS/Desktop的细节暴露到中。
expectactualcommonMainWhen to use this skill
何时使用该技能
Use this when common code needs:
- Permissions, settings, intents, share sheets, deep links, haptics, biometrics, or clipboard.
- Files, paths, clocks, locale, network reachability, sensors, crypto, media, maps, camera, native SDKs, or platform services.
- Native platform views, controllers, or Compose Multiplatform interop.
- Different implementation details on Android, iOS, Desktop, or Wasm while preserving one shared call site.
- A decision between , dependency injection, interfaces, or separate platform code.
expect/actual
当通用代码需要以下能力时适用:
- 权限、设置、意图、分享面板、深度链接、触觉反馈、生物识别或剪贴板。
- 文件、路径、时钟、区域设置、网络可达性、传感器、加密、媒体、地图、相机、原生SDK或平台服务。
- 原生平台视图、控制器或Compose Multiplatform互操作。
- 在Android、iOS、Desktop或Wasm上采用不同实现细节,但保留统一调用入口。
- 在、依赖注入、接口或独立平台代码之间做选择。
expect/actual
Choose the boundary
选择边界方案
| Situation | Prefer |
|---|---|
| Simple compile-time platform specialization | |
| Implementation needs injected dependencies, lifecycle ownership, runtime choice, or test fakes | Common interface plus platform binding |
| UI is mostly shared, one leaf differs | Common composable calling an |
| Entire screen differs by platform | Separate platform screens behind a common navigation contract |
| Only constants/resources differ | Common API exposing semantic values, actual values per platform |
| 场景 | 首选方案 |
|---|---|
| 简单的编译时平台特化 | |
| 实现需要注入依赖、生命周期所有权、运行时选择或测试假实现 | 通用接口加平台绑定 |
| UI大部分共享,仅单个叶子节点不同 | 通用Composable调用 |
| 整个页面因平台而异 | 独立平台页面封装在通用导航契约之后 |
| 仅常量/资源不同 | 通用API暴露语义化值,各平台提供实际值 |
Keep common APIs semantic
保持通用API语义化
Common code should describe what the product needs, not how the platform does it:
kotlin
// GOOD: common API is semantic
expect fun currentRegion(): Regionkotlin
// BAD: common API leaks Android implementation
expect fun currentRegionFromAndroidLocale(context: Context): RegionThe Android actual can use APIs. The iOS actual can use Foundation APIs. Callers should not know.
Locale通用代码应描述产品需求,而非平台实现方式:
kotlin
// 良好示例:通用API语义化
expect fun currentRegion(): Regionkotlin
// 不良示例:通用API暴露Android实现细节
expect fun currentRegionFromAndroidLocale(context: Context): RegionAndroid的actual实现可使用 API,iOS的actual实现可使用Foundation API,调用者无需知晓这些细节。
LocaleKeep actuals thin
保持actual实现轻量化
Actual implementations should translate the semantic API into platform calls. If the operation needs an Activity, view controller, lifecycle owner, DI, or fakes, prefer an interface supplied by platform code instead of an :
expect classkotlin
// commonMain
interface ShareSheet {
suspend fun shareText(text: String)
}kotlin
// 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))
}
}The Android implementation is explicitly Activity-owned. A generic may need and usually hides the UI lifecycle requirement. Define what means: for many platform UI actions it means "the sheet was launched", not "the user completed sharing."
ContextFLAG_ACTIVITY_NEW_TASKsuspendIf the actual starts accumulating business rules, move those rules back to common code and leave only platform translation in the actual.
Actual实现应仅负责将语义化API转换为平台调用。如果操作需要Activity、视图控制器、生命周期所有者、DI或假实现,优先选择由平台代码提供的接口,而非:
expect classkotlin
// commonMain
interface ShareSheet {
suspend fun shareText(text: String)
}kotlin
// 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所有。通用可能需要,且通常会隐藏UI生命周期要求。需明确的含义:对于许多平台UI操作,它表示“面板已启动”,而非“用户完成分享”。
ContextFLAG_ACTIVITY_NEW_TASKsuspend如果actual实现开始积累业务规则,应将这些规则移回通用代码,仅保留平台转换逻辑在actual中。
Prefer interfaces when tests or DI matter
当测试或DI重要时优先使用接口
Use for simple compile-time platform APIs. Use interfaces when common code needs fakes, multiple implementations, runtime selection, or lifecycle ownership:
expect/actualkotlin
interface Clipboard {
suspend fun setText(text: String)
}Platform modules bind to Android/iOS implementations. Common tests use a fake.
Clipboard简单的编译时平台API使用。当通用代码需要假实现、多实现、运行时选择或生命周期所有权时,使用接口:
expect/actualkotlin
interface Clipboard {
suspend fun setText(text: String)
}平台模块将绑定到Android/iOS实现,通用测试使用假实现。
ClipboardCompose-specific guidance
Compose专属指导
- Keep platform-specific Composables at leaf nodes.
- Pass through every expected Composable that emits UI.
Modifier - Avoid platform types in signatures (
commonMain,Context, Android resource IDs,Activity,Uri,Bundle,UIViewController, platform permission enums, etc.).NSBundle - If native view lifecycle matters, hide it inside the platform actual and use the right interop container (,
AndroidView, etc.).UIKitView - Do not launch platform work directly from a Composable body. Use ,
remember,LaunchedEffect, and stable keys inside actual Composables just as you would in common Compose code.DisposableEffect - Make previews/tests use common plain UI composables with fake platform services where possible.
- 将平台特定的Composable放在叶子节点。
- 所有输出UI的预期Composable都要传递。
Modifier - 避免在签名中使用平台类型(
commonMain、Context、Android资源ID、Activity、Uri、Bundle、UIViewController、平台权限枚举等)。NSBundle - 如果原生视图生命周期很重要,将其隐藏在平台actual内部,并使用正确的互操作容器(、
AndroidView等)。UIKitView - 不要直接从Composable主体启动平台任务。在actual Composable中使用、
remember、LaunchedEffect和稳定键,就像在通用Compose代码中一样。DisposableEffect - 尽可能让预览/测试使用通用纯UI Composable,并搭配假平台服务。
Common mistakes
常见错误
| Mistake | Fix |
|---|---|
| Replace with semantic common types |
| Move those details into the actual |
| Business branching duplicated in each actual | Move business rules to common code |
One huge | Split by capability: |
| Platform UI leaks high in the tree | Push platform-specific Composable to a leaf |
| No fakeable boundary for common tests | Use an interface instead of direct |
| 错误 | 修复方案 |
|---|---|
| 替换为语义化通用类型 |
| 将这些细节移至actual实现中 |
| 业务分支逻辑在每个actual中重复 | 将业务规则移至通用代码 |
单个庞大的 | 按能力拆分: |
| 平台UI在树结构中层级过高 | 将平台特定Composable下移至叶子节点 |
| 通用测试无可伪造的边界 | 使用接口替代直接调用 |
Red flags during review
评审时的危险信号
- Common code imports platform packages.
- An actual implementation knows product state, navigation decisions, or domain rules.
- A platform API name appears in a common function name.
- Adding a third platform would require changing common callers.
- Tests need Android/iOS runtime just to verify common business behavior.
- 通用代码导入平台包。
- actual实现知晓产品状态、导航决策或领域规则。
- 平台API名称出现在通用函数名中。
- 添加第三个平台需要修改通用调用者。
- 测试需要Android/iOS运行时才能验证通用业务行为。
Related (Compose / shared UI)
相关内容(Compose / 共享UI)
Stay focused on platform boundaries in this skill; wire shared UI like any other Compose target:
- — shared plain UI composables vs state-holder wiring.
compose-state-holder-ui-split - — effect keys and cleanup in actual composables (
compose-side-effects,LaunchedEffect, etc.).DisposableEffect - and
compose-modifier-and-layout-style— reusable shared Compose APIs (modifiers, slots).compose-slot-api-pattern
本技能聚焦平台边界;共享UI的构建方式与其他Compose目标一致:
- —— 共享纯UI Composable与状态持有者的拆分。
compose-state-holder-ui-split - —— actual Composable中的副作用键与清理(
compose-side-effects、LaunchedEffect等)。DisposableEffect - 和
compose-modifier-and-layout-style—— 可复用的共享Compose API(修饰符、插槽)。compose-slot-api-pattern