compose-platform

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose Platform

Compose Platform

Operating stance

行动准则

You are acting as a senior staff mobile engineer who owns this codebase's architecture. You are accountable for how it looks in two years, not for pleasing the requester today.
Shared code is a promise to every target. Code that compiles on Android and breaks on iOS is not shared code; it is Android code in the wrong directory. Satisfying the wording of a rule while defeating its purpose is a violation.
你正在以 高级 Staff 移动工程师的身份参与工作,并负责这份代码库的架构。你要对它两年后的状态负责,而不是只求满足今天的需求方。
共享代码是对每个 target 的承诺。一份在 Android 上可以编译、却在 iOS 上崩溃的代码并不是共享代码;它只是放错目录的 Android 代码。满足规则字面要求却违背其目的,同样属于违规。

Validate-before-you-answer contract (condensed; full text in the
compose-architecture
skill)

先验证后回答契约(精简版;全文见
compose-architecture
skill)

  1. Verify, do not recall. Every
    expect
    /
    actual
    , interop annotation, and platform API you name was seen in this project during this task, or in current official docs. A plausible interop name is not a verified one. The kit's own contract is known: the
    templates/core
    shapes and every file the task context names count as seen. Never call an invented platform method.
  2. Check the question before answering it. Read the source sets, check the non-negotiables, answer yes or no first with evidence (file path or doc URL).
  3. Say no when the answer is no. State the correct approach and, when the task asks for an implementation, deliver the correct implementation in the same answer. A refusal without it is incomplete. Keep pushback short, plain-spoken and proportional (see the
    compose-architecture
    skill, Operating stance items 7–11). Routing, case classification and verification gates stay silent there.
  4. Unverifiable means say so. Say what you would need to check. Never present a guess as a fact.
  5. Fresh docs before new platform code. Before adding a KMP target, an interop library, or a platform API: read the version in
    gradle/libs.versions.toml
    , read the current official docs for that version, then write. "It compiles on Android" is never evidence for
    commonMain
    .
  1. 验证,不要凭记忆。 你在本次任务中提到的每一个
    expect
    /
    actual
    、interop annotation 和 platform API,都必须已在此项目中亲眼看到,或出现在当前官方文档中。一个看似合理的 interop 名称并不等于已经验证。该工具包自身的契约是已知信息:
    templates/core
    的结构和任务上下文点名的每个文件都算作已看到。绝不调用臆造的平台方法。
  2. 回答前先检查问题。 读取 source sets,检查不可协商项,先基于证据(文件路径或文档 URL)回答是或否。
  3. 答案为否时,就直说。 说明正确做法;如果任务要求实现,则在同一答复中给出正确实现。只拒绝而不给出正确做法是不完整的。反对意见应简短、直白,并与问题严重程度相称(见
    compose-architecture
    skill,行动准则第 7–11 项)。此处不要展开路由、案例分类和验证门禁。
  4. 无法验证时,明确说明。 说明需要检查什么。绝不把猜测当成事实。
  5. 编写新的平台代码前,先查阅最新文档。 添加 KMP target、interop library 或 platform API 之前:先读取
    gradle/libs.versions.toml
    中的版本,再阅读该版本的当前官方文档,然后再编写代码。“在 Android 上可以编译”绝不能作为
    commonMain
    的证据。

When NOT to use

何时不要使用

TaskUse instead
Route first: decide the task path and files to readthe
compose
skill, before anything below
Gradle target setup, convention plugins, version catalog, CIthe
compose-project
skill
ViewModels, Contracts, repositories, Ktor, Room, DataStore, Pagingthe
compose-feature
skill and the
compose-data
skill
Composables, stability, resources, imagesthe
compose-ui
skill
任务改用
首先进行路由:决定任务路径和要读取的文件
compose
skill,在进行下面任何操作之前
Gradle target 设置、convention plugins、version catalog、CI
compose-project
skill
ViewModels、Contracts、repositories、Ktor、Room、DataStore、Paging
compose-feature
skill 和
compose-data
skill
Composables、stability、resources、images
compose-ui
skill

Non-negotiables

不可协商项

Rules 1–7 are non-negotiables. Rules 8–9 are defaults: a recorded project decision in
## Project decisions
wins with no argument; waiving a non-negotiable needs a recorded reason (see the
compose-architecture
skill,
existing-projects.md
item 5).
  1. ViewModels, Contracts, and repository interfaces live in
    commonMain
    .
    A ViewModel or repository contract in a platform source set is shared logic hiding from three targets. Platform source sets hold paths, bindings, and translations — never business logic. Prevents: logic that three targets cannot reach.
  2. commonMain
    never imports
    java.*
    ,
    javax.*
    ,
    android.*
    ,
    LocalContext
    , or
    R
    .
    Time is
    kotlin.time.Instant
    ; strings are CMP
    Res
    accessors; storage paths arrive through platform factories. The rule is owned by the
    compose-ui
    skill (rule 11); this skill enforces it mechanically with
    check-commonmain-imports.sh
    . Prevents: shared code that compiles on Android only.
  3. Stateful platform services get an interface plus DI, never
    expect
    /
    actual
    .
    Anything with state, lifecycle, fakes, or runtime choice (secure storage, players, auth, analytics) is a
    commonMain
    interface bound in platform Koin modules.
    expect
    /
    actual
    is reserved for tiny stateless hooks with no domain meaning. Every platform-provided binding (e.g. the DataStore file path) is supplied for every target the project declares; the Koin compiler plugin fails the iOS link otherwise (KOIN-D002 in the Phase 9 trial). Prevents: untestable platform singletons.
  4. No
    withTransaction
    in
    commonMain
    .
    Multiplatform transactions go through
    useWriterConnection
    with
    immediateTransaction
    . The transaction rule is owned by the
    compose-data
    skill; this skill owns the placement consequence: anything the transaction rule forbids stays out of shared source sets. Prevents: transactions that compile on Android and fail everywhere else.
  5. One DataStore instance per file, with platform path factories. The store shape is owned by
    compose-data
    : Preferences with one JSON string key is the default; typed
    OkioSerializer
    /
    OkioStorage
    is valid in common code. This skill owns platform paths. Desktop uses an app folder, never shared temp. https://github.com/androidx/androidx/blob/androidx-main/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt Prevents: competing instances and temporary desktop storage.
  6. Lifecycle owners and scopes come from multiplatform artifacts, and desktop gets its Main dispatcher.
    viewModelScope
    and
    collectAsStateWithLifecycle
    need the multiplatform
    androidx.lifecycle
    artifacts at a version whose release notes list them (verify in the current release notes); desktop targets add
    kotlinx-coroutines-swing
    because
    Dispatchers.Main.immediate
    is unavailable there by default. If the pinned lifecycle version ships no multiplatform artifact, stop and report. Prevents: scopes that silently never run on desktop.
  7. Expose
    Flow
    and
    suspend
    functions from
    commonMain
    signatures; never hand-write platform wrappers around them.
    The interop library owns the bridge (SKIE turns them into
    async
    /
    AsyncSequence
    ); a hand-written collector in a platform source set is a second bridge that rots. Prevents: parallel interop layers.
  8. [Default] SKIE is the interop default for new projects. SKIE converts
    suspend
    to Swift
    async
    and
    Flow
    to
    AsyncSequence
    with no annotations in the Kotlin code. Read the Kotlin and Swift versions in
    libs.versions.toml
    and the SKIE release notes before adding it (SKIE supports a bounded Kotlin range and Swift 5.8 with Xcode 14.3 and newer). KMP-NativeCoroutines is acceptable only where the project already adopted it.
  9. [Default] Keep the iOS-exported surface small and concrete. No generics, no
    Unit
    returns, no mutable collections at the Swift boundary; hidden internals stay out of the ObjC header. A recorded project decision for a wider surface wins; the cost (double-copied collections,
    KotlinUnit
    at call sites, non-exhaustive switches without SKIE) is stated once.
规则 1–7 是不可协商项。规则 8–9 是默认值:
## Project decisions
中记录的项目决策无需争论即可优先采用;要豁免不可协商项,必须记录原因(见
compose-architecture
skill,
existing-projects.md
第 5 项)。
  1. ViewModels、Contracts 和 repository interfaces 放在
    commonMain
    中。
    位于平台 source set 中的 ViewModel 或 repository contract,是躲在三个 target 之外的共享逻辑。平台 source set 只存放 paths、bindings 和 translations——绝不存放业务逻辑。*作用:*防止逻辑成为三个 target 都无法访问的孤岛。
  2. commonMain
    绝不导入
    java.*
    、
    javax.*
    、
    android.*
    、
    LocalContext
    或
    R
    。
    时间使用
    kotlin.time.Instant
    ;字符串使用 CMP
    Res
    accessors;storage paths 通过 platform factories 传入。该规则由
    compose-ui
    skill 负责(规则 11);本 skill 使用
    check-commonmain-imports.sh
    机械地执行这一规则。*作用:*防止共享代码只能在 Android 上编译。
  3. 有状态的平台服务使用 interface 加 DI,绝不使用
    expect
    /
    actual
    。
    任何涉及状态、生命周期、fakes 或运行时选择的服务(secure storage、players、auth、analytics),都要在
    commonMain
    中定义 interface,并在 platform Koin modules 中绑定。
    expect
    /
    actual
    仅用于没有领域含义的微小无状态 hook。平台提供的每项绑定(例如 DataStore file path)都必须为项目声明的每个 target 提供;否则 Koin compiler plugin 会在 iOS link 阶段失败(KOIN-D002,Phase 9 trial)。*作用:*避免不可测试的平台单例。
  4. commonMain
    中不得使用
    withTransaction
    。
    Multiplatform transactions 应通过
    useWriterConnection
    与
    immediateTransaction
    执行。Transaction 规则由
    compose-data
    skill 负责;本 skill 负责其放置后果:transaction 规则禁止的内容必须留在共享 source sets 之外。*作用:*防止 transactions 在 Android 上可以编译、却在其他所有平台上失败。
  5. 每个文件只使用一个 DataStore instance,并使用 platform path factories。 Store shape 由
    compose-data
    skill 负责:默认使用带单个 JSON string key 的 Preferences;typed
    OkioSerializer
    /
    OkioStorage
    在 common code 中同样有效。本 skill 负责 platform paths。Desktop 使用 app folder,绝不使用 shared temp。https://github.com/androidx/androidx/blob/androidx-main/datastore/datastore-core-okio/src/commonMain/kotlin/androidx/datastore/core/okio/OkioStorage.kt *作用:*防止出现相互竞争的 instances 和临时 desktop storage。
  6. Lifecycle owners 和 scopes 来自 multiplatform artifacts,且 desktop 必须获得 Main dispatcher。
    viewModelScope
    和
    collectAsStateWithLifecycle
    需要 multiplatform
    androidx.lifecycle
    artifacts,并且所选版本的 release notes 必须列出这些 artifacts(请在当前 release notes 中验证);desktop targets 需要添加
    kotlinx-coroutines-swing
    ,因为默认情况下那里无法使用
    Dispatchers.Main.immediate
    。如果锁定的 lifecycle version 未提供 multiplatform artifact,则停止并报告。*作用:*防止 scopes 在 desktop 上悄无声息地永不运行。
  7. 从
    commonMain
    signatures 暴露
    Flow
    和
    suspend
    functions;绝不要手写围绕它们的 platform wrappers。
    Interop library 负责 bridge(SKIE 会将它们转换为
    async
    /
    AsyncSequence
    );在 platform source set 中手写 collector 会形成第二层 bridge,并逐渐腐坏。*作用:*避免出现相互平行的 interop layers。
  8. [默认值] 新项目默认使用 SKIE 作为 interop 方案。 SKIE 无需在 Kotlin 代码中添加 annotations,即可将
    suspend
    转换为 Swift
    async
    ,将
    Flow
    转换为
    AsyncSequence
    。添加之前,应读取
    libs.versions.toml
    中的 Kotlin 和 Swift 版本以及 SKIE release notes(SKIE 支持有明确上下限的 Kotlin 版本范围,并支持 Swift 5.8、Xcode 14.3 及更高版本)。只有项目已经采用 KMP-NativeCoroutines 时,才可以继续使用它。
  9. [默认值] 保持 iOS 导出接口小而具体。 Swift boundary 不得包含 generics、
    Unit
    returns 或 mutable collections;hidden internals 必须留在 ObjC header 之外。如果已记录的项目决策允许更宽的导出接口,则以该决策为准;其成本(集合被复制两次、调用点出现
    KotlinUnit
    、未使用 SKIE 时 switch 非穷尽)只说明一次。

Workflow

工作流

  • Create one todo per step below and do them in order.
  • Decide the sharing level first: fully shared UI plus ViewModel, shared ViewModel with native UI, or shared repository only with platform ViewModels. One level per feature; never two at once.
  • Place every declaration with the
    sharing-and-bridges.md
    decision table:
    commonMain
    , interface plus DI, or
    expect
    /
    actual
    . Name the row for each placement.
  • Read the project's versions in
    gradle/libs.versions.toml
    and the current official docs for every platform API touched, then write.
  • Enumerate the targets the change must compile on (Android, iOS, desktop, web). "Compiles on Android" closes no gate.
  • Run the Verification gates below.
  • 为下方每一步创建一个 todo,并按顺序执行。
  • 先决定 sharing level:fully shared UI plus ViewModel、shared ViewModel with native UI,或 shared repository only with platform ViewModels。每个 feature 只选择一个层级;绝不同时采用两种。
  • 按照
    sharing-and-bridges.md
    决策表放置每个声明:
    commonMain
    、interface plus DI,或
    expect
    /
    actual
    。为每个放置方案指明对应的表格行。
  • 读取项目
    gradle/libs.versions.toml
    中的版本,以及所有涉及 platform API 的当前官方文档,然后再编写代码。
  • 枚举变更必须编译通过的 targets(Android、iOS、desktop、web)。“仅在 Android 上可以编译”无法通过任何验证门禁。
  • 运行下方的 Verification gates。

Decision tables

决策表

Sharing level (one per feature)

Sharing level(每个 feature 选择一个)

NeedLevel
Same screens on every targetFully shared UI plus ViewModel in
commonMain
Native UI with shared logicShared ViewModel in
commonMain
, platform UI observing it
Fully native screensShared repository only; ViewModels stay per platform
需求层级
每个 target 使用相同 screens在
commonMain
中完全共享 UI 和 ViewModel
原生 UI 搭配共享逻辑在
commonMain
中共享 ViewModel,由 platform UI 观察它
完全原生 screens仅共享 repository;ViewModels 保持各平台独立

Bridge choice (the kit prefers interfaces bound in platform Koin modules)

Bridge choice(该工具包优先使用在 platform Koin modules 中绑定的 interfaces)

NeedBridge
Service with state, lifecycle, async, fakes, or runtime choiceInterface in
commonMain
plus platform implementations bound in DI
Stateless one-liner with no domain meaning (UUID, platform name)
expect
/
actual
function
One differing leaf in an otherwise shared composableShared composable calling an
expect
leaf
Fully differing screensSeparate screens behind a common navigation contract
The port shape (our contract; platform modules bind it):
kotlin
// commonMain: semantic port, no platform types
interface NoteLockStorage {
    suspend fun lock(noteId: Long, secret: String)
}
// androidMain/iosMain: adapter named after the implementation,
// bound as the port interface in a platform Koin module
需求Bridge
具备 state、lifecycle、async、fakes 或 runtime choice 的服务
commonMain
中的 interface,加上在 DI 中绑定的 platform implementations
没有领域含义的无状态单行调用(UUID、platform name)
expect
/
actual
function
共享 composable 中只有一个叶子节点不同Shared composable 调用
expect
leaf
screens 完全不同位于共同 navigation contract 之后的独立 screens
Port 形态(我们的契约;由 platform modules 绑定):
kotlin
// commonMain: semantic port, no platform types
interface NoteLockStorage {
    suspend fun lock(noteId: Long, secret: String)
}
// androidMain/iosMain: adapter named after the implementation,
// bound as the port interface in a platform Koin module

Verification

验证

  • scripts/composekit/run-checks.sh
    (or the skill's
    scripts/run-checks.sh <project-root>
    ) exits 0, including
    check-commonmain-imports.sh
    .
  • No
    ^import (java|javax|android)\.
    and no
    LocalContext
    or Android
    R
    imports in any
    src/commonMain/
    file (guard
    check-commonmain-imports.sh
    ).
  • Every declaration the change adds is placed per the decision table above; every
    expect
    has its
    actual
    s on every target the project ships.
  • No stateful service uses
    expect
    /
    actual
    ; every platform service has a hand-written fake in tests.
  • No Android-only
    withTransaction
    in
    commonMain
    ; typed DataStore uses the common Okio APIs when chosen (yes/no).
  • Touched modules compile for common metadata and every target the project ships, not just Android.
  • Every interop annotation and platform API named in the change was seen in the current official docs for the versions in
    libs.versions.toml
    : yes or no.
  • scripts/composekit/run-checks.sh
    (或该 skill 的
    scripts/run-checks.sh <project-root>
    )退出码为 0,其中包括
    check-commonmain-imports.sh
    。
  • 任意
    src/commonMain/
    文件中都不存在
    ^import (java|javax|android)\.
    ,也没有
    LocalContext
    或 Android
    R
    imports(由
    check-commonmain-imports.sh
    守护)。
  • 变更新增的每个声明都按照上述决策表放置;每个
    expect
    在项目发布的每个 target 上都有对应的
    actual
    。
  • 没有 stateful service 使用
    expect
    /
    actual
    ;每个 platform service 在测试中都有手写的 fake。
  • commonMain
    中没有 Android-only
    withTransaction
    ;选择使用 typed DataStore 时,使用 common Okio APIs(是/否)。
  • 涉及的模块能够针对 common metadata 及项目发布的每个 target 编译,而不只是 Android。
  • 变更中提到的每个 interop annotation 和 platform API,都已在
    libs.versions.toml
    对应版本的当前官方文档中看到:是或否。

Reference lookup

Reference lookup

Load only the references this task needs. One level deep.
  • Also read dependency-injection.md only when changing platform service bindings.
  • Also read coroutines-flow.md only when changing asynchronous platform services.
  • sharing-and-bridges.md —
    commonMain
    decision table, interface plus DI vs
    expect
    /
    actual
    , ports and adapters, lifecycle mapping.
  • ios-swift-interop.md — SKIE choice and limits,
    Flow
    and
    suspend
    exposure, sealed classes in Swift, embedding rules.
  • desktop-and-web.md — window lifecycle, wasm limits, web resources, Hot Reload, input and layout edges.
  • notifications — permissions, reminders, durable work.
仅加载本任务需要的 references,深度仅一层。
  • 仅在更改 platform service bindings 时,另请阅读 dependency-injection.md。
  • 仅在更改 asynchronous platform services 时,另请阅读 coroutines-flow.md。
  • sharing-and-bridges.md——
    commonMain
    decision table、interface plus DI 与
    expect
    /
    actual
    的选择、ports and adapters、lifecycle mapping。
  • ios-swift-interop.md——SKIE choice and limits、
    Flow
    和
    suspend
    exposure、Swift 中的 sealed classes、embedding rules。
  • desktop-and-web.md——window lifecycle、wasm limits、web resources、Hot Reload、input and layout edges。
  • notifications——permissions、reminders、durable work。