docs/, so you can read a step next to the tree it builds.
The manual setup below is what Modaal automates. Every tree this tutorial opens and every file it asks you to write is produced by the Duet templates in the Modaal new-project wizard, and a coding agent working in Modaal writes the feature, records it and runs the checks for you. This series walks the same ground by hand so you know what the scaffold emits and why: start a Duet project in the wizard when you would rather skip the setup.
What will you build?
The app is Foyer: a splash screen with a safety net, a sign-in gate, a three-step onboarding gate, a home with one paid feature behind an entitlement check, a three-step upgrade flow, and a profile tree three levels deep. The screens are generic; the flows are the ones most consumer apps share. Every arrow in the diagram below is a delegate event received as a parent action, every screen is mounted from state, and the paid check reads one entitlement value that only an entitlement stream writes. The backend is on the device: four ports (auth, purchases, items, account) with local implementations that persist to a JSON file. Running any tree needs no account, no store connection and no network.The nine tutorials
Tutorial 1: Your First Feature
State, actions and effects as data; a pure reducer; a scenario recorded into four fixtures and replayed on the host lane; the test-clock suite. No UI.
Tutorial 2: One Behavior, Two Apps
The Kotlin core built into an Apple framework, the four recordings replayed across the Swift boundary, then a SwiftUI app and a Compose app that play the same splash from the same core and land on the same placeholder screen.
Tutorial 3: Composing Features
A parent level, children mounted from state, a child’s delegate events received as parent actions, chain scenarios pinning the sign-in gate and a sign-out climbing four levels, and the four ports behind per-platform mock services.
Tutorial 4: Workers
An on-device backend replacing the mock services one for one; session and entitlement streams observed by workers adopted at mount and tested with the worker harness; the entitlement projected down as a slice; the home’s locked card unlocking from the stream.
Tutorial 5: Navigation as State
Route state, back as an action, process-death restore on Android, a deep link parsed into route state, and onboarding steps as siblings sharing their progress through lateral state.
Tutorial 6: The Checks in CI
Lanes and the manifest, the parity workflow in your own repository, and the mutation drill run both directions.
Tutorial 7: Theming with Design Tokens
A token vocabulary, generated accessors on both platforms, and a second theme that changes no feature code.
Tutorial 8: Localizing the App
String catalogs on iOS, resources on Android, semantic values in shared state, and recordings that do not change with a translation.
Tutorial 9: Adding Analytics
The app’s event verbs emitted from reducers as effect data, a console sink worker, and events asserted in scenarios.
What do you need installed?
- macOS with Xcode 26.6. Tutorial 1 uses none of it; Tutorial 2 onward builds the iOS app and the Kotlin/Native framework it consumes.
- A JDK, version 25. The Gradle wrapper in every tree fetches Gradle itself, and the Kotlin compiler arrives as a Gradle plugin; there is no separate Kotlin install.
- Android Studio’s SDK from Tutorial 2 onward, for the Compose app. Set up the Android toolchain lists the components.
- XcodeGen (
brew install xcodegen) from Tutorial 2 onward. The iOS app’s Xcode project is generated fromsrc-ios/App/xcodegen.yml. - The
duetcommand-line tool is fetched by each tree’stools/duetwrapper at the pinned version on first use.
Which versions are the tutorials verified against?
Every tree resolves the same family set, written once in the repository’spins.env: Duet 0.7.0, duet-tools 0.24.0 and duet-services 0.11.1, with Kotlin 2.4.10 and Gradle 9.7.1. The repository’s CI runs every tree’s checks on every push, so a tree that stops passing at those versions fails the build there rather than on your machine. When the family moves, the trees are re-pinned in one change and the repository is tagged for the set they were verified against. Two pages add pins of their own: Tutorial 2 adds SKIE 0.10.14, the Swift projection of the Kotlin/Native framework, and Tutorial 3 adds the KSP mock processor 0.2.1 and the Sourcery mocks bundle 0.6.2.
What does the series teach?
One loop, repeated per feature. Write the feature’s state, actions and effect payloads as data and its reducer as a pure function in Kotlin. Describe its behavior as a scenario in the fixture-authoring language, and record the scenario into fixture files. The Kotlin lane replays the fixtures against the reducer; from Tutorial 2, the Apple boundary lane replays the same fixtures through the framework the iOS app consumes, and CI fails when either lane disagrees with the bytes on disk. Each app keeps its own native interface and its own platform code for anything the shared logic cannot own. The Duet overview states the architecture; the glossary defines each term the tutorials use. The pages call a recorded file a recording; theduet tool, the manifest and the parity/fixtures/ directory call the same file a fixture.
The two tables below map each concept the series teaches to the tutorials that treat it, so you can open the step that introduces a concept without reading the series in order; each link lands on that step. The first table covers Duet’s own vocabulary, each term defined in the glossary.
The second table covers the engineering practice the series exercises along the way.
Common questions
Do I need to know Kotlin and Swift both?
Do I need to know Kotlin and Swift both?
Tutorial 1 is Kotlin only. From Tutorial 2 each page shows the Swift and Kotlin halves of every per-platform step in tabs; the shared logic stays Kotlin throughout. Reading SwiftUI and Compose is enough to follow; the shells are short.
Can I start at a later tutorial?
Can I start at a later tutorial?
Yes. Open that tutorial’s
-start tree from the repository; it is the previous tutorial’s finished state plus the exercise stub. Tutorials 7 to 9 all start from Tutorial 6’s finished tree.Why is the backend on the device?
Why is the backend on the device?
So every tree runs with no account, no store and no network, and so the feature logic reaches the outside world only through the four port interfaces. A real backend implements the same ports; nothing in the feature modules changes.
What is the difference between this series and the Modaal templates?
What is the difference between this series and the Modaal templates?
The trees the series opens have the shape the Duet templates in the Modaal new-project wizard emit. The series writes by hand what the scaffold and a coding agent produce for you, so you can read an emitted project and know what each file is for.
Sources and further reading
- The duet-tutorials repository — the step trees, their checks, the pins file, and
docs/, which holds these nine pages as Markdown. - The Duet framework repository — the kernel, the test harness and the framework’s contracts.
- The duet-tools repository — the
duetcommand-line tool the trees run their checks with, and the manifest grammar. - Kotlin Multiplatform documentation — the technology under the shared core.
Read next
Tutorial 1: Your First Feature
Write the splash feature in Kotlin, record its behavior and verify it, with no UI yet.
Duet: one shared core, two native apps
What Duet is, what you get, and the two project cards that scaffold it.