kotlin-multiplatform-expect-actual

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Kotlin Multiplatform: expect/actual boundaries

Kotlin Multiplatform:expect/actual边界

Core principle

核心原则

Keep common APIs semantic and stable. Put platform mechanics behind small
expect
/
actual
declarations or interfaces, and keep Android/iOS/Desktop details out of
commonMain
.
保持通用API的语义化与稳定性。将平台机制封装在小型
expect
/
actual
声明或接口之后,避免将Android/iOS/Desktop的细节暴露到
commonMain
中。

When 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
    expect/actual
    , dependency injection, interfaces, or separate platform code.
当通用代码需要以下能力时适用:
  • 权限、设置、意图、分享面板、深度链接、触觉反馈、生物识别或剪贴板。
  • 文件、路径、时钟、区域设置、网络可达性、传感器、加密、媒体、地图、相机、原生SDK或平台服务。
  • 原生平台视图、控制器或Compose Multiplatform互操作。
  • 在Android、iOS、Desktop或Wasm上采用不同实现细节,但保留统一调用入口。
  • expect/actual
    、依赖注入、接口或独立平台代码之间做选择。

Choose the boundary

选择边界方案

SituationPrefer
Simple compile-time platform specialization
expect
/
actual
function, value, typealias, or leaf composable
Implementation needs injected dependencies, lifecycle ownership, runtime choice, or test fakesCommon interface plus platform binding
UI is mostly shared, one leaf differsCommon composable calling an
expect
leaf
Entire screen differs by platformSeparate platform screens behind a common navigation contract
Only constants/resources differCommon API exposing semantic values, actual values per platform
场景首选方案
简单的编译时平台特化
expect
/
actual
函数、值、类型别名或叶子Composable
实现需要注入依赖、生命周期所有权、运行时选择或测试假实现通用接口加平台绑定
UI大部分共享,仅单个叶子节点不同通用Composable调用
expect
叶子节点
整个页面因平台而异独立平台页面封装在通用导航契约之后
仅常量/资源不同通用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(): Region
kotlin
// BAD: common API leaks Android implementation
expect fun currentRegionFromAndroidLocale(context: Context): Region
The Android actual can use
Locale
APIs. The iOS actual can use Foundation APIs. Callers should not know.
通用代码应描述产品需求,而非平台实现方式:
kotlin
// 良好示例:通用API语义化
expect fun currentRegion(): Region
kotlin
// 不良示例:通用API暴露Android实现细节
expect fun currentRegionFromAndroidLocale(context: Context): Region
Android的actual实现可使用
Locale
API,iOS的actual实现可使用Foundation API,调用者无需知晓这些细节。

Keep 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 class
:
kotlin
// 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
Context
may need
FLAG_ACTIVITY_NEW_TASK
and usually hides the UI lifecycle requirement. Define what
suspend
means: for many platform UI actions it means "the sheet was launched", not "the user completed sharing."
If 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 class
kotlin
// 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所有。通用
Context
可能需要
FLAG_ACTIVITY_NEW_TASK
,且通常会隐藏UI生命周期要求。需明确
suspend
的含义:对于许多平台UI操作,它表示“面板已启动”,而非“用户完成分享”。
如果actual实现开始积累业务规则,应将这些规则移回通用代码,仅保留平台转换逻辑在actual中。

Prefer interfaces when tests or DI matter

当测试或DI重要时优先使用接口

Use
expect/actual
for simple compile-time platform APIs. Use interfaces when common code needs fakes, multiple implementations, runtime selection, or lifecycle ownership:
kotlin
interface Clipboard {
    suspend fun setText(text: String)
}
Platform modules bind
Clipboard
to Android/iOS implementations. Common tests use a fake.
简单的编译时平台API使用
expect/actual
。当通用代码需要假实现、多实现、运行时选择或生命周期所有权时,使用接口:
kotlin
interface Clipboard {
    suspend fun setText(text: String)
}
平台模块将
Clipboard
绑定到Android/iOS实现,通用测试使用假实现。

Compose-specific guidance

Compose专属指导

  • Keep platform-specific Composables at leaf nodes.
  • Pass
    Modifier
    through every expected Composable that emits UI.
  • Avoid platform types in
    commonMain
    signatures (
    Context
    ,
    Activity
    , Android resource IDs,
    Uri
    ,
    Bundle
    ,
    UIViewController
    ,
    NSBundle
    , platform permission enums, etc.).
  • If native view lifecycle matters, hide it inside the platform actual and use the right interop container (
    AndroidView
    ,
    UIKitView
    , etc.).
  • Do not launch platform work directly from a Composable body. Use
    remember
    ,
    LaunchedEffect
    ,
    DisposableEffect
    , and stable keys inside actual Composables just as you would in common Compose code.
  • Make previews/tests use common plain UI composables with fake platform services where possible.
  • 将平台特定的Composable放在叶子节点。
  • 所有输出UI的预期Composable都要传递
    Modifier
  • 避免在
    commonMain
    签名中使用平台类型(
    Context
    Activity
    、Android资源ID、
    Uri
    Bundle
    UIViewController
    NSBundle
    、平台权限枚举等)。
  • 如果原生视图生命周期很重要,将其隐藏在平台actual内部,并使用正确的互操作容器(
    AndroidView
    UIKitView
    等)。
  • 不要直接从Composable主体启动平台任务。在actual Composable中使用
    remember
    LaunchedEffect
    DisposableEffect
    和稳定键,就像在通用Compose代码中一样。
  • 尽可能让预览/测试使用通用纯UI Composable,并搭配假平台服务。

Common mistakes

常见错误

MistakeFix
commonMain
API exposes Android/iOS types
Replace with semantic common types
expect
function has parameters for one platform only
Move those details into the actual
Business branching duplicated in each actualMove business rules to common code
One huge
Platform
expect object
Split by capability:
Clipboard
,
ShareSheet
,
Haptics
Platform UI leaks high in the treePush platform-specific Composable to a leaf
No fakeable boundary for common testsUse an interface instead of direct
expect
call
错误修复方案
commonMain
API暴露Android/iOS类型
替换为语义化通用类型
expect
函数包含仅适用于单个平台的参数
将这些细节移至actual实现中
业务分支逻辑在每个actual中重复将业务规则移至通用代码
单个庞大的
Platform
expect对象
按能力拆分:
Clipboard
ShareSheet
Haptics
平台UI在树结构中层级过高将平台特定Composable下移至叶子节点
通用测试无可伪造的边界使用接口替代直接调用
expect

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:
  • compose-state-holder-ui-split
    — shared plain UI composables vs state-holder wiring.
  • compose-side-effects
    — effect keys and cleanup in actual composables (
    LaunchedEffect
    ,
    DisposableEffect
    , etc.).
  • compose-modifier-and-layout-style
    and
    compose-slot-api-pattern
    — reusable shared Compose APIs (modifiers, slots).
本技能聚焦平台边界;共享UI的构建方式与其他Compose目标一致:
  • compose-state-holder-ui-split
    —— 共享纯UI Composable与状态持有者的拆分。
  • compose-side-effects
    —— actual Composable中的副作用键与清理(
    LaunchedEffect
    DisposableEffect
    等)。
  • compose-modifier-and-layout-style
    compose-slot-api-pattern
    —— 可复用的共享Compose API(修饰符、插槽)。