Lesson 11
Package and verify the toolkit
Turn the learning implementation into a reusable library with explicit public boundaries and layered evidence.
Learning goal
Turn the learning implementation into a reusable library with explicit public boundaries and layered evidence.
Final architecture
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 --> DriverContractPublic versus internal API
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 .-> InternalA 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
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
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
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: 1Assert 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 completeawaitTerminationwithFailedwithout canceling the caller; - a frame driver returning
nullcompletes 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
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:
- Atlas composables emit only Atlas nodes.
- Runtime maintains those nodes through
AtlasApplier. - state mutation causes recomposition and correct property/structural changes;
- layout and painting translate committed nodes into
SurfaceFrame; - a
Surfaceimplementation presents those frames; - typed events can update state through Atlas interaction policy;
- deterministic tests drive snapshot notifications, Compose frames, events, rendering, and termination without a native backend;
- supported
commonMaintargets compile with public/internal visibility intact; - shutdown is idempotent and critical failures propagate observably;
- 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
- Why does the session create a child job instead of canceling the caller’s job?
- Which objects are caller-owned, and how would an adapter explicitly transfer ownership?
- 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.