Lesson 2

Create a multiplatform foundation

Place the reusable toolkit engine in Kotlin Multiplatform `commonMain` and keep platform integration behind narrow contracts.

3 min readUpdated Jul 17, 2026

Learning goal

Place the reusable toolkit engine in Kotlin Multiplatform commonMain and keep platform integration behind narrow contracts.

Source-set map

Diagram
flowchart TD
    commonMain[commonMain<br/>nodes · Applier · composables<br/>layout · frames · semantics]
    commonTest[commonTest<br/>tree · layout · frame tests]
    mac[macosArm64Main<br/>optional adapter]
    linux[linuxX64Main<br/>optional adapter]
    win[mingwX64Main<br/>optional adapter]

    mac -->|depends on| commonMain
    linux -->|depends on| commonMain
    win -->|depends on| commonMain
    commonTest -. tests .-> commonMain

The arrows follow source-set dependency direction: platform source sets consume shared policy from commonMain.

Build shape

A representative build.gradle.kts looks like this:

plugins {
    kotlin("multiplatform") version "<kotlin-version>"
    id("org.jetbrains.kotlin.plugin.compose") version "<same-kotlin-version>"
    id("org.jetbrains.compose") version "<compose-multiplatform-version>"
}

kotlin {
    macosArm64()
    linuxX64()
    mingwX64()

    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>")
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>")
        }
    }
}

Use real, compatible versions rather than the placeholders. With Kotlin 2.x, the Compose compiler Gradle plugin version should match the Kotlin version. The Compose Multiplatform plugin exposes compose.runtime for common code.

The target declarations communicate intended library targets. Native target support changes over time, so check the current Kotlin and Compose compatibility matrices before pinning versions. Tests and final binaries run on a suitable host for each target; Apple final binaries require macOS tooling. Those packaging constraints do not change the toolkit architecture.

Dependency rule

Diagram
flowchart LR
    App --> PublicAPI[atlas-ui composable API]
    PublicAPI --> Engine[common engine]
    PublicAPI --> Runtime[Compose Runtime annotations]
    Engine --> Runtime
    Engine --> Contract[Surface contract]
    Adapter[platform/surface adapter] --> Contract
    Adapter -. must not own .-> EnginePolicy[layout or composition policy]

High-level toolkit policy depends on the stable Surface contract. An adapter implements that contract; it does not reach into Runtime or decide how composables are interpreted.

Suggested package layout

atlas-ui/
├── src/commonMain/kotlin/atlas/
│   ├── surface/       Surface, SurfaceFrame, SurfaceCommand
│   ├── node/          AtlasNode, containers, leaf nodes
│   ├── runtime/       AtlasApplier, AtlasHost
│   ├── layout/        constraints, measurement, placement
│   ├── render/        tree-to-frame painter
│   ├── input/         events, focus, semantics
│   └── component/     Text, Group, Button
├── src/commonTest/kotlin/atlas/
│   └── ...
└── src/<platform>Main/kotlin/atlas/adapter/
    └── optional real adapters

This is a responsibility map, not a requirement to create every package immediately. Begin with one file when a concept is small; split only when ownership becomes clearer.

Why a recording backend comes first

A real backend introduces unrelated failures: native handles, event loops, device negotiation, threading, or graphics APIs. A recording backend asks only whether the Compose-to-surface pipeline produced the intended frame.

Diagram
flowchart LR
    Test --> AppContent
    AppContent --> Compose
    Compose --> AtlasTree
    AtlasTree --> RecordingSurface
    RecordingSurface --> Assertion[assert commands]

This is not a throwaway mock. It is a reference implementation of the surface contract and a long-term conformance tool for adapters.

Platform-neutral does not mean platform-free

Some owner must eventually provide:

  • a frame opportunity or clock;
  • a size and lifecycle;
  • input events, if the surface is interactive;
  • a way to present a frame.

Atlas UI names those requirements as interfaces. Lessons use deterministic implementations so the core can be learned without native APIs.

Visual checkpoint

Draw the source-set diagram and circle every box allowed to import Compose Runtime. The common engine may import Runtime; a thin adapter should normally only implement Atlas contracts.

Review questions

  1. Why is RecordingSurface part of the architecture rather than merely a test trick?
  2. Which source set owns layout rules?
  3. What information must cross from a real adapter into common code?

Recap

commonMain owns: toolkit meaning and policy
adapters own:    communication with a concrete surface
commonTest owns: deterministic proof of the shared engine

Sources

Finished this lesson?

Your progress stays only in this browser.