Solution

    One core. Two native apps. No shared UI layer.

    Duet shares what should be on screen and nothing about how it looks. iOS renders SwiftUI, Android renders Compose, and neither knows the other exists. A test fails the build if the two ever disagree about behaviour.

    Here is the number that matters, from a production app.

    When Memory Lane added Android, zero logic files changed. Not "few". None. The only additions were six island entries — three regions across two platforms — for the places where the platforms genuinely had to differ.

    That is what this page is about: the contract that makes it possible, and why we did not solve it the way most cross-platform frameworks do.

    How it works

    1. 01

      Logic is shared and pure

      Features are reducers over typed state, with effects as data. One implementation, both platforms.

    2. 02

      What is on screen is a value

      The two platforms agree on which surface should be showing, expressed as state. They agree on nothing about how it renders.

    3. 03

      Each platform renders itself

      iOS renders SwiftUI. Android renders Compose. There is no shared UI layer, no bridge, no renderer in the middle.

    4. 04

      Divergence has to be declared

      Platforms cannot quietly drift apart. Where they differ, someone writes it down and a person reviews it.

    Why there is no shared UI layer

    This is where Duet differs from Flutter, React Native and every framework built on the same idea.

    Those tools share the interface as well as the logic, rendering one UI on both platforms. It is a reasonable trade and for plenty of apps it is the right one. But it means your interface is a compromise between two design languages, you wait for the framework whenever a platform ships something new, and platform-specific behaviour arrives as an escape hatch rather than the default.

    Duet shares the layer above that. The two platforms agree on which surface should be on screen — expressed as a value in feature state — and agree on nothing else. Detents, animations, corner radii, chrome, transitions: all of it is per-platform and unconstrained. None of it leaks into shared state.

    In practice that means an Android ModalBottomSheet and an iOS .sheet are the same surface as far as your app's logic is concerned, and completely different things as far as the user is concerned. Which is correct, because a user on Android is expecting a bottom sheet.

    The same applies to system integrations. Memory Lane uses Android's system Photo Picker and an in-app library on iOS. Different components, different code, same surface in the shared model.

    What the two platforms actually agree on

    The contract has five layers, and only the first three are shared.

    Structure. Four primitives — path, slot, tabs, stage — express every navigation shape the app can take. That set is deliberately closed: adding to it requires a versioned contract change, not a quiet edit.

    Kinds and verbs. Each surface in your app is an enum case carrying a plain-value payload. Every structural change happens through a named action, and the platform hosts never write structure directly. The rule the docs state most firmly: no silent transitions. If the navigation state changed, something named said so.

    Manner — the third layer — is where sharing stops. Animation curves, corner radii, sheet behaviour, chrome. Entirely per-platform, entirely unconstrained.

    Islands are the fifth layer: opaque regions like media pagers and camera sessions that declare their boundary but keep their internals private. Memory Lane has three of them.

    Parity is a test, not a promise

    Every cross-platform tool claims your platforms will behave the same. Duet's version of that claim is checkable, and this is the part we would most like you to take away.

    Both platforms replay the same recorded interactions, and the resulting state has to match exactly — byte for byte, or the build fails. Not "looks similar in review". Not "we checked the important flows". A test that goes red.

    That inverts the usual failure mode. In two hand-maintained codebases, divergence is the default: someone fixes a rounding bug on iOS, Android keeps it for two releases, and nobody decided that would happen. Here, divergence in shared behaviour is impossible to introduce by accident, because the fixtures catch it before the code lands.

    Where the platforms genuinely should differ, the difference is written into a ledger and reviewed by a person. So a divergence is always a decision somebody made, never something that quietly happened.

    The technical detail — the presentation registry, the fixture format, how islands are ledgered — is in the cross-platform UI parity documentation, including the full Memory Lane walkthrough.

    One visual identity, two native implementations

    A deliberate choice worth being clear about, because it is not what most people assume.

    Android runs a Material 3 scheme tuned to the iOS palette — the same warm paper and bark primary, in light and dark — rather than porting design tokens across or defaulting to stock Material.

    So the two apps look like the same product. They do not look like a port of each other. The components are genuinely native on each side — real Compose, real SwiftUI, built from each platform's own primitives — while the colour, weight and feel stay recognisably yours across both.

    If what you want instead is each platform looking maximally like its own platform defaults, Duet is not trying to do that, and it is worth knowing before you start.

    What adding a surface looks like

    Five steps, and the order matters.

    Define the surface as an enum case. Record a fixture before implementing any UI. Name the reducer verbs for what the user intends, not for what moves. Bind the platform renderers in each composition root. Ledger any place the two platforms need to behave differently in manner.

    The fixture-first ordering is the part that does the work. You are describing the behaviour you expect before either platform has an opinion about how to draw it — so the shared model stays about what, and the platforms keep full freedom over how.

    Where you can start

    You can begin from either side. Bring an existing Modaal iOS app across, start both platforms together, or start Android-first and add iOS later. Nothing about the shared core assumes a primary platform.

    Both outputs are ordinary native projects on your own disk. The iOS side opens in Xcode, the Android side in Android Studio. No proprietary format, no export step — the code was never anywhere else.

    Android is in open beta now, with general availability in September 2026.

    Frequently asked questions

    Duet is how Modaal builds iOS and Android from one shared core. The application logic is shared and pure — features are reducers over typed state — and the two platforms agree on what should be on screen, expressed as a value. They share nothing about how it renders: iOS uses SwiftUI, Android uses Compose, and there is no cross-platform UI layer between them.

    Those frameworks share the interface as well as the logic, rendering one UI on both platforms. Duet shares the layer above that — which surface should be showing — and lets each platform render it natively with its own components, animations and conventions. An Android bottom sheet and an iOS sheet are the same surface to your app’s logic and completely different things to the user, which is how it should be.

    A test. Both platforms replay the same recorded interactions and the resulting state has to match exactly — byte for byte — or the build fails. Where the platforms genuinely should differ, that difference is written into a ledger and reviewed by a person. So divergence in shared behaviour cannot be introduced by accident, and intentional divergence is always a decision somebody made.

    When Memory Lane, a production app, added Android, zero logic files changed. The only additions were six island entries — three regions across two platforms — covering the places the platforms genuinely had to differ, such as Android’s system Photo Picker versus an in-app library on iOS. Those stayed contained in the composition roots.

    They look like the same product, not like a port of each other. Android runs a Material 3 scheme tuned to the iOS palette — the same warm paper and bark primary, in light and dark — rather than porting design tokens or defaulting to stock Material. The components are genuinely native on each side; the visual identity is shared.

    Modaal Free builds one platform of your choice — iOS or Android — with unlimited prompts and no expiry, so you can see the native output before paying. Duet, meaning both platforms from one shared core, is part of Pro at €9 a month billed annually.

    Yes. You can bring an existing Modaal iOS app across, start both together, or start Android-first and add iOS later. Nothing in the shared core assumes a primary platform.

    Yes. Modaal generates ordinary native projects on your own disk — the iOS side opens in Xcode, the Android side in Android Studio. There is no proprietary format and no export step, because the code was never anywhere else.

    Start building for free

    Build for as long as you want on the Free plan. Only upgrade when you're ready to launch.