Skip to main content
This series teaches Duet, Modaal’s cross-platform parity framework for native iOS and Android apps, by building one app from its first feature to its analytics. You write each feature’s logic once in Kotlin, mount it in a SwiftUI app and a Jetpack Compose app, and record its behavior so a CI gate replays the same recordings on both platforms. Nine tutorials, each with a starting tree you open and a finished tree you can compare against, all in the public duet-tutorials repository. The same nine pages are in that repository as Markdown under 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.
Tutorials 1 to 6 build on each other in order. Tutorials 7, 8 and 9 each start from Tutorial 6’s finished tree, so you can take them in any order. Every starting tree from Tutorial 2 onward is the previous tutorial’s finished tree plus one deliberately failing test, the closing exercise; Tutorial 1 starts from an empty project.

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 from src-ios/App/xcodegen.yml.
  • The duet command-line tool is fetched by each tree’s tools/duet wrapper 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’s pins.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; the duet 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

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.
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.
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.
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

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.
Last modified on September 9, 2026