The parts of a feature
feature
A unit of behavior declared as state, actions, effect payloads and a pure reducer, carried as one row in the parity manifest and checked by its own behavior recordings. See how a feature gets built.reducer
Where every decision happens: for each action the Store feeds it, the reducer alone runs the guards, writes state and returns effects. It is pure — no platform calls, no minted time or identity — so its behavior replays byte-for-byte on both platforms.effect / effect payload
What a reducer returns beside the new state: a value naming work the platform must do, such as a wait on a clock, a port call or a delegate notification, never the work itself. The effect handler turns each payload into the actions it produces, and an effect id gives it cancel-in-flight semantics. Because an effect is data, a behavior recording pins it byte for byte. Tutorial 1 writes the first two; Tutorial 9 adds an analytics event as one more.Store
Built once per mount: it holds the feature’s state, takes actions, runs the reducer, and hands returned effects to the effect handler. Nothing else runs feature logic. Tutorial 1 builds the first one and drives it through the kernel’s exhaustive test store.kernel
The runtime every Store is built on: it runs each action through the reducer, tracks returned effects in flight, and owns the effect-leak check at teardown. One kernel shape serves both platforms; its contract ships with the framework.shell
What stays on the platform side once logic has moved: three duties and no decisions — bind the view’s intents to the store, project state into the view, bracket the lifecycle. See what crossing one feature looks like; Tutorial 2 writes the first Swift and Compose shells.host / host root
The level that mounts a feature: it builds the store, owns the slot the feature renders in, and takes its delegate events as data. In the Swift flavor a host root is the package whose resolved dependencies must contain the Duet family and nothing else. Tutorial 3 mounts a tree of features from state, one level hosting the next.delegate events / the delegate seam
How a feature reports an outcome upward: the reducer emits a plain enum case, and the host receives it as data on the seam it handed in at build time. The event is a value, so a scenario asserts it byte-for-byte. Tutorial 3 receives each child’s delegate events as one case of the parent’s action type.Interactor
The CombineRIBs type that owns a RIB’s business logic. In a migration every Interactor shrinks to a shell and is then deleted. See what happens to each piece of your code.worker
Stateful, lifecycle-bound processing on the environment side, adopted at mount: its results re-enter feature state only through declared seams, and cancellation is its stop. Workers process, reducers decide; a worker has no golden recording. Tutorial 4 writes the first two and tests them with the worker harness.package
In the Swift flavor a feature’s subtree holds two Swift packages: the gated feature half resolves the Duet family and nothing else, so the checks build it in isolation; the app-facing node half is where the app connects.port
An interface the shared logic calls and each app implements, such as the auth, purchases, items and account ports in the tutorials’ app. Every operation starts work and returns, with the answer arriving through a callback that fires once, so a Swift class can implement it across the framework boundary; a stream on a port is a sticky value a worker observes. Tutorial 3 declares the four ports behind mock services; Tutorial 4 adds the streams and replaces the mocks with an on-device backend.slice
A value a parent publishes downward out of its own state, delivered to a child as an action of the child’s own: the child reads it and never writes it, and nothing below the parent holds the parent’s Store. Tutorial 4 projects the entitlement to two tabs as a slice.lateral state
A value that must cross between sibling features, neither of which holds the other’s store: one worker owned by their lowest common ancestor, with a void update on one side and a sticky observation on the other. Tutorial 5 builds one for the onboarding steps’ readiness.@MainActor
The escalation a handler or worker takes to cross an isolation boundary. Marking a type unchecked-Sendable instead silences the compiler without proving anything, which is why the doctor pass flags it on a worker.
the fixture-authoring language
The four verbs a scenario is written in — given, when, then, then-effects — with a Swift and a Kotlin spelling. The framework’s pages call it the scenario DSL.Recording and checking
scenario
One test in the fixture-authoring language, pinning one outcome. Mutually exclusive endings are branches over one starting state; a chain scenario pins a delegate seam that crosses features. Tutorial 1 writes the first scenario and records it.behavior recording
What recording a scenario produces: a checked-in file replaying the scenario’s exact state and effect bytes — a build product, never hand-edited, and it wins any disagreement with spec prose. The tools, the manifest and the framework’s pages call these files fixtures.golden recording
A behavior recording in its replay role: the golden test reads committed recordings at runtime and fails on any byte that differs — a role, not a second kind of file. A worker has no golden.chain recording
A behavior recording that pins the seam between two features rather than one reducer: the delegate event one reducer emits, received by the next as its action, with each hop re-derived at verify time from the recorded bytes so an edited seam fails as structure drift. Tutorial 3 records the first four.re-recording
Running the recorder again after a deliberate behavior change, then reading the recording diff like code. The run deletes before it harvests, so a leftover file means a stale run.metadata-only
The pass condition for a re-record that must not change behavior: the diff touches scenario source locations and step labels, and zero state or effect bytes.mutation drill
The negative control on the checks: break a reducer guard and watch the replay go red, then break a recording file and watch the test task go red — proof the checks are wired to what they claim to cover. Run in a crossing’s first half; Tutorial 6 runs it from a mutation table in both directions.the checks
What must pass before work is done, run by one command locally and in CI: the manifest lint, the recording check, the cross-platform verification and the doctor pass. The framework’s pages call each one a gate. Tutorial 6 lists every check with what it proves and puts them in one CI workflow.lane
One slice of a checks run, rooted at a specific package or module set, so a red names its platform and its root. Parity lanes derive from a feature’s manifest row; generation lanes produce the test doubles and DI components. Tutorial 6 reads the lanes report the tool derives.the Swift and Kotlin lanes
A manifest row’s two implementation declarations; which of them a row carries tells the checks which platform lanes to run — one during ordinary work, both inside a coexistence window.the manifest lint
The manifest’s own consistency checks — the fast red, run on every manifest edit. The declaration-parity and subtree-geometry checks join only while a row declares both implementations.the headless test runner
A script every scaffolded project ships that runs the iOS test suite from the command line, wrapping Apple’s xcodebuild — for verifying outside the Modaal app and reproducing CI failures locally.the run verb
The build-and-launch action in a Modaal session: one command that builds the selected target and launches it on a simulator, emulator or device. The action’s name differs between session types, so Duet’s documents say “the run verb”.The migration, step by step
the readiness audit
A static census of a legacy app’s distance to the target shape, producing the readiness report, the migration tracker and a spec draft per feature. The counts are pointers to verify, not verdicts. See the stages, step 1.the migration tracker
One row per surface: a derived status, four sizing columns, and the notes the per-feature plan is written into. The tools and the migration article call it the ledger — distinct from the divergence waivers the parity contract keeps.updating the tracker
Re-running the readiness audit in write mode: it creates or merges the tracker and refreshes the computed fields. It is the route’s first step after the stop-digging edit.the graft
The one operation that adds the Kotlin core plane, the Android app shell and the parity workspace beside an existing tree while editing none of its sources — the checks exist before any behavior can change. See the stages, step 2.wiring the core
Run once the graft’s checks are green: it links the assembled Kotlin core into the iOS app’s build, so every later flip is feature work with no build wiring left to add. See the stages, step 2b.feature crossing
One feature’s move onto the shared Kotlin core, end to end: spec, Swift twin, Kotlin twin, the Android mount, the flip. See what crossing one feature looks like.the coexistence window
The stretch of a crossing in which a feature’s manifest row declares both implementations: both twins replay the same recordings and the parity lints run. A valid resting state for as long as the repository wants.the flip
The one change that closes a coexistence window: delete the Swift twin, make the feature’s row Kotlin-only, verify both platforms.twin
One of a feature’s two implementations while its coexistence window is open — the Swift twin inside the app, the Kotlin twin in the shared core — replaying the same behavior recordings.single-source
The destination shape: one implementation of a feature’s logic, in the shared Kotlin core, consumed by both apps. The Swift flavor’s single-core is the same one-copy rule before a second platform consumes it. See the two Duet templates.The work items and the field kit
surface
A tracker row’s unit of work: one RIB or one legacy worker in the app being converted, typed by the row’s kind.reference test
The existing test a conversion is written against, ported one-to-one onto the deterministic test store or the worker test harness. A surface without one earns its reference test first; the audit’s sizing table and the tracker call it the oracle.look-and-feel work
The part of a surface that does not move: tuned rendering, animation timing, gesture handling — re-authored on the other platform and sized by counting files. The audit calls it platform-manner mass; manner is the contract layer it lives in.the note format
The shape every tracker note takes: a capitalized tag, an em dash, then clauses following the surface’s kind — a RIB’s note carries a crossing position; a worker’s names the crossing it rides with.the stop-digging rule
From step 0 on, a new feature in the legacy app is authored in the target shape — a reducer plus a thin shell, never a new Router — and new background work is a Duet worker from day one. See the stages, step 0.adoption seam
Where a converted feature’s SwiftUI view attaches to the legacy UIKit composition: a themed hosting controller takes the legacy child’s slot, and activating the shell replaces attaching the child.coexistence kit
The migration package that stands for the migration’s duration — the worker shim and the dual-import discipline — and whose dependency edge is deleted at completion. See the finish line.stage host
What the Android app’s root grows into when the app needs real navigation: one full-screen child chosen from route state, transitions as reducer edges, back handling in a documented order. The first crossing that needs a screen change grows it.mount
Putting a feature into the app’s composition: its Store is built once at the mounting level, its screen renders from the mount row, and that level’s own workers are adopted there.spine
The app’s navigation trunk, in two uses: the route state that survives process death, and the root and navigation surfaces that cross last, after the leaves. Tutorial 5 gathers a route spine and rebuilds the tree from it.Common questions
Why does my repository say fixture where this page says behavior recording?
Why does my repository say fixture where this page says behavior recording?
They are the same file. The headings here use the documentation’s names; the tools, the parity manifest and the framework’s pages use the framework-side names — fixture, gate, ledger, oracle — and each entry states both: behavior recording, the checks, the migration tracker, reference test.
How long can a coexistence window stay open?
How long can a coexistence window stay open?
As long as the repository wants — the window is a valid resting state, and the parity lints keep both twins honest while it is open. Holding one open costs the dual-language authoring premium the migration article quantifies; the flip closes it.
Where is each term treated in full?
Where is each term treated in full?
Three articles carry the depth: the Duet overview for the feature shape and the two templates, handling platform-specific UI for the presentation contract, and migrating a CombineRIBs app for the route. The framework’s own reference lives in the Duet repository. The nine Duet tutorials build each feature-level term by hand, and the series index maps every concept to the step that treats it.
How do I start a Duet project?
How do I start a Duet project?
Pick one of the two Multiplatform cards in the Modaal new-project wizard — starting a Duet project walks through the choice.
Sources and further reading
- The Duet framework repository — the framework API, the emitted contracts and the family’s own documentation.
- Kotlin Multiplatform documentation — the technology under the Kotlin Multiplatform flavor’s shared core.
- Swift concurrency documentation — the isolation model behind the
@MainActorentry.
Read next
Duet: one shared core, two native apps
The product overview: what a Duet project is, the two templates, and how a feature gets built.
Handling platform-specific UI
The five-layer presentation contract: kinds, manner, islands, and how deliberate divergence is recorded.
Migrating a CombineRIBs app
The route the migration cluster’s entries walk: audit, graft, crossings, the finish line.
Duet tutorials
Feature, reducer, shell, worker, scenario, recording and the checks, each built by hand across nine tutorials.