What are the three ways to ship two apps?
Every iOS-and-Android product takes one of three routes:- Write both apps in full. A Swift codebase and a Kotlin codebase, each implementing every rule. Two teams (or one team twice), full platform fidelity, and every behavior change made twice.
- Share everything, including the interface. One implementation in one language — Flutter, React Native, or Compose Multiplatform — with the framework supplying the widgets on both platforms.
- Share the logic, keep the interface native. One implementation of the rules and flows; two thin interface layers, each in its platform’s own toolkit. Google supports Kotlin Multiplatform for exactly this split, and it is the route Duet takes — Duet is Modaal’s cross-platform parity framework for native iOS and Android apps.
How do you know both apps behave the same?
The three routes give three different answers, and the answer is the real decision:- Identity by convention — two full codebases. The same rule is implemented twice, and agreement rests on code review and manual QA finding every divergence. Nothing mechanical fails when the two apps drift apart.
- Identity by construction — one shared implementation. There is nothing to compare, so agreement is automatic — up to the boundary. Where the shared code calls into per-platform code (a platform channel, a native module), the guarantee stops, and that boundary code is back to identity by convention.
- Identity by verification — two implementations (or one shared core consumed twice) checked against the same executable contract. In Duet, each feature’s behavior is recorded as fixture files; both apps replay them and CI compares the results byte-for-byte, so a divergence is a failing build rather than a bug report.
Who draws the screen × where the logic runs. The bottom-left cell is the share-the-logic route; the survey page walks every placement.
What does “business logic” mean concretely?
Business logic is the set of decisions that must not differ between platforms: what a tap is allowed to do, what happens when a payment fails mid-flow, in which order results land, what gets persisted and when. In a Duet feature it is literally one pure reducer — current state and an action in, new state and requested effects out — with clocks, network and storage behind effects that platform workers perform. Pixels are not business logic. Screen layout, navigation chrome, animation, haptics and typography are where iPhone and Android users hold different expectations — Apple’s Human Interface Guidelines on one side, Material 3 on the other — and where sharing produces an app that feels foreign on at least one platform. The split holds even when a screen is data-driven: what is on screen is a value in shared state; how it is rendered stays per platform. Handling platform-specific UI covers where that line sits case by case.What does sharing the logic cost?
Two costs, stated plainly:- The views are written twice. Each feature gets a SwiftUI screen and a Jetpack Compose screen. The logic, navigation decisions, effect handling and the test corpus are written once, so the second view layer is rendering work against behavior that already passes.
- A change touching both platforms costs more than one platform. In Modaal’s own runs, a behavior change shipped to both apps measured roughly 40% more agent effort than the single-platform equivalent — not the 100% of doing everything twice, and the delta is concentrated in the second view layer.
What do you get for it?
- One bug, fixed once. A logic defect lives in one implementation. The fix ships to both stores from one edit.
- Behavior parity you can point at. The recorded fixtures are an artifact — replayed on both platforms on every push, byte-compared in CI. “Do the apps agree?” has a mechanical answer, not an opinion.
- Native look and feel on both platforms. Each app uses its platform’s toolkit, inherits its accessibility behavior, and adopts new OS releases on the platform’s schedule — there is no cross-platform widget layer to wait on.
- An exit that costs nothing. Both apps are ordinary native codebases in one repository; the shared core is a library either app could in principle replace. Nothing is hostage to a framework’s runtime.
Who should not share code?
- Single-platform products. If Android (or iOS) is genuinely never coming, the shared core’s discipline still buys fast, deterministic tests, but the parity machinery is overhead you chose without the payoff.
- Games and heavily drawn interfaces. A physics loop or a custom-rendered playfield is not reducer-shaped work, and the drawn surface is the product — a shared canvas framework or a game engine fits better. Modaal’s own guidance says the same: its 2D game template is not a Duet template.
- Teams committed to two independent native teams. Sharing logic couples the teams’ release trains at the core. If your organization deliberately staffs two autonomous teams and accepts double implementation as the price of independence, that is a coherent position — it is identity by convention, and manual QA is its verification budget.
Common questions
Isn't 'write once, run anywhere' cheaper than any of this?
Isn't 'write once, run anywhere' cheaper than any of this?
A single shared implementation is the cheapest way to get two app icons on two stores, and for interface-light products it is a good trade — the survey page’s best-fit row says so. The costs arrive later and are specific: the interface is the framework’s on both platforms, and shared code that crosses into per-platform modules leaves the guarantee. Compare the routes on those terms, not on launch cost alone.
What about the web?
What about the web?
Sharing logic with a web app is the same decision one platform wider, and Kotlin and Rust cores both compile for it. Duet targets iOS and Android; if web-plus-mobile from one codebase is the requirement today, that is a real advantage of Flutter and Compose Multiplatform — see the survey page’s platform-coverage section.
Duet projects are scaffolded by Modaal — starting a Duet project walks through the two template cards.
Sources and further reading
- Kotlin Multiplatform on developer.android.com — Google’s support statement for sharing business logic between Android and iOS
- JetBrains Kotlin Multiplatform case studies — Forbes, McDonald’s, Cash App and others on the shared-logic shape
- Crux and UniFFI — the same shared-core shape with Rust
- Apple Human Interface Guidelines and Material 3 — the two design systems a native interface answers to
Read next
The best cross-platform frameworks in 2026
Every mainstream option on one axis — who draws, where logic lives, what proves agreement — with a best-fit row.
Duet overview
The product behind the third route: one shared core, two native apps, and a CI gate that fails when they disagree.
Handling platform-specific UI
Where the shared/native line sits on the rendering side, and how deliberate divergence is recorded.
How to build testable Android apps
The same architecture argued from the testing side: pure reducers, effects as data, recorded scenarios.
Duet tutorials
The third route in code: nine tutorials build one app in Kotlin, SwiftUI and Compose, with recordings gating both apps.
iOS or Android first?
Which platform to ship first when you are not shipping both, on modaal.dev.