What stays standard Android?
The toolchain is unchanged: Kotlin, Jetpack Compose, Gradle, and a tree Android Studio opens and runs — nothing proprietary in it, no code generator you cannot leave. The architecture is the one Google’s own app architecture guide recommends in general form — unidirectional data flow, state as data, a clear UI/logic boundary. What Duet changes is who enforces it: the shape is scaffolded and gated rather than described in a document, and the logic layer compiles tocommonMain so the iPhone app in the same repository runs the identical code. Inside a Duet Android app walks the resulting tree file by file.
What does a blank project leave open that Duet fixes?
A new Android project fixes almost nothing about structure: each screen’s author chooses where logic sits, how state is held, and what a test can reach. Duet replaces those per-screen choices with three project-wide ones:- One feature shape. Every feature is a serializable state, a sealed set of actions, and one pure reducer — no hand-rolled ViewModel variants, no per-screen state idiom.
- Effects as data. A reducer never calls a repository, reads a clock, or generates an ID. It returns effect values; workers perform them and report back as actions. Anything nondeterministic round-trips through that seam, which is what makes behavior replayable.
- Recordings as the test contract. Each feature carries a scenario — a
Given/When/Thenscript that is compiled into fixture files. The fixtures are build products checked into the repository, and CI replays them byte-for-byte on every platform that implements the feature.
The same behavior, hand-rolled and fixed
A typical hand-rolled ViewModel, and the failure modes a test has to fight:FavoritesViewModel.kt (a blank-project idiom)
FavoritesFeature.kt (commonMain)
SaveSucceeded. And the test is no longer a class with fakes — it is a script over the reducer, recorded once and replayed forever:
FavoritesScenarioTest.kt
How do the two setups compare?
What does it cost?
The fix has three costs; the Duet overview carries the full list:- The architecture is fixed at creation. A Duet project stays a Duet project; the conventions — the feature shape, the effect seam, the recording step — are not per-feature choices.
- Views are still written per platform. The testability applies to logic. Compose screens are ordinary Compose, tested the way the platform tests UI, and an Android-plus-iOS change was measured at roughly 40% more agent effort than the single-platform equivalent in Modaal’s own runs.
- The recording step is part of the loop. Changing behavior means updating the scenario and re-recording, not just editing code until the app looks right. That is the point — but it is a step a blank project does not have.
What happens when Android kills your app’s process?
Android can kill an app’s process while it is in the background and recreate the activity from a savedBundle — an asymmetry iOS developers meet on day one of an Android port, documented in Google’s saved-state guidance. In this architecture the answer is uniform: state is serializable data, so the navigation spine is captured as a value in onSaveInstanceState and restored into the rebuilt tree — the anatomy page shows the dozen lines in the activity that do it. There is no per-screen inventory of what to save, because there is no per-screen state idiom.
Why does this suit AI coding agents?
An agent building a feature needs machine-checkable gates between its steps, or errors compound silently across a session. The scaffold’s loop supplies them in a fixed order: the feature spec, then the scenario, then the recording, then the reducer that must satisfy it, then the screens — withtools/duet verify as the same gate CI runs. An agent (or a person) cannot wire a screen to logic that has no passing recording, and a behavior regression fails the build on the commit that caused it rather than surfacing in QA. This staged order is how Modaal’s own agent builds every Duet feature — the Duet overview walks the six steps.
What are the measured results?
From Memory Lane, the shipped dual-platform app this site’s Duet pages quote, each number with its scope:- Warm test suite: 1–3 seconds for the dual-platform behavior suite at four-feature scale during the field migration — fast enough to run on every edit.
- Mutation drill: 10 out of 10. Ten seeded logic mutations, each caught by fixture replay at the recording layer.
- Recordings byte-identical across platforms: 6 of 6 in the adoption measurement — the Kotlin core replays the exact fixture bytes the Swift reducers recorded.
- Sixteen Android screens, zero logic edits. The Android app’s product surfaces were built against a core whose recordings already passed; no fixture was re-recorded to make Android work.
When should you not use Duet?
Two cases are outside its lane today. Games and drawn playfields: no Duet card yet scaffolds a SpriteKit scene host, and a physics loop is not reducer-shaped work — Modaal’s 2D game / Interactive app template covers that ground, and Duet support for graphics-rich apps is planned. And teams that want to assemble a bespoke stack — their own DI framework, their own test strategy, per-screen architectural freedom — are choosing exactly the degrees of freedom Duet removes; a blank Android Studio project serves that intent better.Common questions
Is this MVI?
Is this MVI?
It is in the same family — unidirectional flow with state as data and actions as the only inputs. What a generic MVI setup does not fix is where nondeterminism lives or what proves behavior: Duet adds the effect seam as a hard rule and recorded fixtures as the contract, replayed on both platforms in CI.
How is this different from ViewModel tests with fakes?
How is this different from ViewModel tests with fakes?
A ViewModel test exercises code against fakes you maintain, and it lives on one platform. A scenario drives a pure reducer with no fakes at all, and recording it produces an artifact — the fixture — that both the Android and iPhone implementations must replay byte-for-byte. The test suite is also a cross-platform agreement check, not only a regression net.
Do I lose Android Studio, Compose previews, or Jetpack libraries?
Do I lose Android Studio, Compose previews, or Jetpack libraries?
No. The project is a standard Gradle build; Android Studio opens it, Compose screens are ordinary Compose, and Jetpack libraries live where they always did — in the app module’s workers and screens. The constraint is only that feature logic stays in its module and reaches I/O through effects.
What happens to in-flight work when the system kills the process?
What happens to in-flight work when the system kills the process?
The state that must survive is serializable and rides the saved-instance Bundle; effects in flight are re-requested by the restored state where the feature models it that way. The scenario can pin that behavior — restoration after a system kill is a branch you record, not a bug class you meet in production.
Can I use Duet for an Android-only app?
Can I use Duet for an Android-only app?
Structurally yes — the Android app and the shared core work with no iOS target. The recording discipline still pays (fast pure tests, a mutation-resistant suite), but the cross-platform gate is Duet’s distinctive return; for a product that will only ever ship one platform, weigh the conventions against a lighter setup.
Modaal scaffolds Duet projects that start from this architecture instead of retrofitting it.
Sources and further reading
- Guide to app architecture — Google’s recommendation: UI layer, data layer, unidirectional data flow
- Save UI states — what must survive when the system recreates the activity or kills the process, and how
- Test apps on Android — the platform’s testing fundamentals this page’s approach narrows
- Thinking in Compose — state-driven UI on the rendering side of the seam
- The Duet framework on GitHub — both flavors, the recording toolchain and the versioned contracts
Read next
Inside a Duet Android app
The tree this page’s rules produce: feature modules, the thin app module, workers and the fixtures directory.
Duet overview
What Duet is, the wizard cards that scaffold it, and the six-step loop every feature follows.
Handling platform-specific UI
Where the shared/native line sits on the rendering side, and how deliberate divergence is recorded.
Duet glossary
Reducer, worker, scenario, fixture, the checks — every term defined with one link each.
Duet tutorials
The rules on this page applied by hand: a pure reducer and its recordings in Tutorial 1, the worker harness in Tutorial 4, the mutation drill in Tutorial 6.