Lesson 11

Package and verify the toolkit

Turn the learning implementation into a reusable library with explicit public boundaries and layered evidence.

6 min readUpdated Jul 17, 2026

Learning goal

Turn the learning implementation into a reusable library with explicit public boundaries and layered evidence.

Final architecture

Diagram
flowchart TB
    subgraph App
      Content[Atlas composables]
      State[application state]
    end

    subgraph Library[atlas-ui commonMain]
      Components[public components]
      Host[AtlasHost]
      Lifecycle[terminal state + cleanup]
      RuntimeBridge[ComposeNode + AtlasApplier]
      Nodes[internal retained nodes]
      Layout[layout + painter]
      SurfaceContract[Surface / SurfaceFrame]
      InputContract[EventSink / SurfaceEvent]
      DriverContract[FrameDriver]
      Router[event router]
      Semantics[semantics + focus]
    end

    subgraph Adapters
      Recording[recording adapters]
      Concrete[real surface adapters]
    end

    TestHarness[test harness]
    State --> Content --> Components --> RuntimeBridge --> Nodes --> Layout --> SurfaceContract
    InputContract --> Router --> State
    DriverContract --> Host --> RuntimeBridge
    Host -->|owns| Router
    Host -->|owns| Lifecycle
    Lifecycle -->|cancel / dispose / join| RuntimeBridge
    Semantics --> Nodes
    Semantics --> Router
    Recording -. implements .-> SurfaceContract
    Recording -. implements .-> DriverContract
    Concrete -. implements .-> SurfaceContract
    Concrete -. feeds .-> InputContract
    TestHarness --> Host
    TestHarness --> SurfaceContract
    TestHarness --> InputContract
    TestHarness --> DriverContract

Public versus internal API

Diagram
flowchart LR
    Public[Public<br/>components · read-only frame values<br/>Surface contracts · host factory]
    Internal[Internal<br/>mutable nodes · dirty flags<br/>Applier details · caches]
    Public --> Internal
    Consumer -. cannot depend on .-> Internal

A practical public API might expose:

// Components
@Composable @AtlasComposable fun Text(...)
@Composable @AtlasComposable fun Group(...)
@Composable @AtlasComposable fun Button(...)

// Read-only contracts; submitted frame storage must not be mutated.
fun interface Surface { fun present(frame: SurfaceFrame) }
fun interface FrameDriver { suspend fun nextFrameNanos(): Long? }
enum class EventDispatch { Accepted, Full, Closed }
fun interface EventSink {
    fun dispatch(event: SurfaceEvent): EventDispatch
}

sealed interface SessionEnd {
    data object Closed : SessionEnd
    data class Failed(val cause: Throwable) : SessionEnd
}

// Controlled, asynchronous lifecycle.
interface AtlasSession {
    val events: EventSink
    suspend fun setContent(content: AtlasContent)
    suspend fun awaitTermination(): SessionEnd
    suspend fun shutdown()
}

fun createAtlasSession(
    surface: Surface,
    frameDriver: FrameDriver,
    parentContext: CoroutineContext,
): AtlasSession

The factory creates a session-owned SupervisorJob parented to parentContext, serializes all tree access on its owner context, and cancels only its own job. Each critical child catches and records failure before the session cancels siblings, so awaitTermination can return Failed without failure escaping upward and canceling the caller. Cancellation of the external parent still closes the child session.

EventSink writes to a bounded channel. Accepted events are processed FIFO in channel acceptance order; concurrent producers do not receive an additional source-order guarantee. A full queue returns Full, and dispatch after terminal shutdown returns Closed, allowing adapters to retry, coalesce, or drop according to documented policy. The injected Surface and FrameDriver remain caller-owned unless a separate closeable adapter bundle explicitly transfers ownership.

Mutable nodes and AtlasApplier can remain internal unless adapter authors genuinely need them. Exposing them early makes invariants part of the compatibility promise.

Dependency direction

Diagram
flowchart BT
    Adapter --> Contract
    Component --> NodeAPI
    RuntimeBridge --> NodeAPI
    Layout --> NodeAPI
    NodeAPI --> Values[read-only values]

No adapter should import component implementations. No component should import a concrete adapter. Runtime details should not leak into Surface.

Evidence pyramid

Diagram
flowchart TD
    Unit[Many fast tests<br/>Applier · constraints · semantics] --> Integration[Composition tests<br/>state → retained tree → frame]
    Integration --> Conformance[Adapter conformance<br/>same frames/events]
    Conformance --> Real[Small real-surface smoke suite]

1. Structural tests

Prove insert/remove/move/clear, parent links, and illegal attachment behavior.

2. Layout and frame tests

Use fixed constraints and compare typed values, not platform screenshots.

assertEquals(
    listOf(
        SurfaceCommand.DrawText(
            origin = SurfacePoint(0, 0),
            text = "Count: 1",
            color = AtlasColor(0xFFFFFFFFu),
        )
    ),
    recordingSurface.latestFrame().commands,
)

3. Composition integration test

Diagram
sequenceDiagram
    participant Test
    participant State
    participant Host
    participant Tree
    participant Recorder

    Test->>Host: setContent(Counter)
    Test->>Host: advanceUntilIdle()
    Host->>Recorder: frame with Count: 0
    Test->>State: count = 1
    Test->>Host: advanceUntilIdle()
    Host->>Tree: update same TextNode
    Host->>Recorder: frame with Count: 1

Assert both output and retained identity. This is the final proof of the Compose integration boundary.

4. Host lifecycle tests

Use deterministic failures to verify:

  • composition/effect, Surface.present, and frame-driver exceptions complete awaitTermination with Failed without canceling the caller;
  • a frame driver returning null completes normally;
  • repeated or concurrent shutdown is idempotent and waits for cleanup;
  • accepted events preserve order, full/closed dispatch is reported, and dispatch-versus-shutdown races are safe;
  • caller-owned adapters remain open while explicitly transferred resources are released.

5. Adapter conformance

Create a shared suite for any concrete adapter:

  • presents frames in order;
  • preserves command data or documents supported reductions;
  • converts resize and activation into Atlas events;
  • stops callbacks and releases resources owned by the adapter when the session ends;
  • reports unsupported capabilities explicitly.

Failure boundaries

Diagram
flowchart TD
    ComposeFailure[composition/effect failure] --> Session[AtlasSession owner]
    RenderFailure[surface.present failure] --> Session
    DriverFailure[frame driver stops] --> Session
    Session --> Cleanup[dispose composition<br/>cancel and join runtime work<br/>release owned adapter resources]
    Session --> Report[complete awaitTermination]

The library should not silently wait forever after a critical child coroutine fails. Critical child completion enters one terminal-state path, awaitTermination reports normal closure or failure, and shutdown is idempotent. A FrameDriver returning null represents normal end-of-stream; a thrown exception represents failure.

Definition of success

You have covered a new surface when all of these are true:

  1. Atlas composables emit only Atlas nodes.
  2. Runtime maintains those nodes through AtlasApplier.
  3. state mutation causes recomposition and correct property/structural changes;
  4. layout and painting translate committed nodes into SurfaceFrame;
  5. a Surface implementation presents those frames;
  6. typed events can update state through Atlas interaction policy;
  7. deterministic tests drive snapshot notifications, Compose frames, events, rendering, and termination without a native backend;
  8. supported commonMain targets compile with public/internal visibility intact;
  9. shutdown is idempotent and critical failures propagate observably;
  10. a real backend can be replaced without changing component or Runtime code.

Visual checkpoint

Redraw the final architecture from memory. Then cover the “Adapters” box. The remaining diagram should still show composition, nodes, layout, outbound frames, inbound events, frame driving, lifecycle ownership, and the test harness. If it does, the platform boundary is healthy.

Review questions

  1. Why does the session create a child job instead of canceling the caller’s job?
  2. Which objects are caller-owned, and how would an adapter explicitly transfer ownership?
  3. What evidence proves the full loop without a concrete platform backend?

Where to go next

Choose one direction based on evidence:

  • add richer layout policies;
  • add a modifier system with ordered, immutable elements;
  • add text measurement as an injected surface capability;
  • implement one concrete adapter;
  • add adapter capability negotiation;
  • investigate a Compose UI port only if existing Compose UI compatibility becomes a requirement.

Do not expand all directions at once. Each adds a new policy surface and compatibility promise.

Recap

Compose Runtime is the incremental state-to-tree engine.
Atlas UI supplies the meaning of the tree.
The Surface contract isolates external presentation.
Recording adapters make the architecture provable.
Concrete platform work is a replaceable final step.

Finished this lesson?

Your progress stays only in this browser.