Skip to main content
Share the business logic when you ship both platforms and your product’s value is in its behavior — rules, flows, money, data. Keep the interface native on each platform, because the interface is where the platforms genuinely differ. The rest of this page is the case for that split: the three ways to ship two apps, what each way can prove about behavior, what sharing costs in practice, and the products for which sharing is the wrong call.

What are the three ways to ship two apps?

Every iOS-and-Android product takes one of three routes:
  1. 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.
  2. 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.
  3. 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.
The third route is established practice, not an experiment: Forbes, McDonald’s, Cash App and Google Docs ship shared Kotlin logic under native interfaces, per JetBrains’ case studies and Google’s Kotlin Multiplatform page, and the same shape exists with a Rust core (Mozilla’s UniFFI bindings, Signal’s libsignal).

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.
Two-by-two grid placing cross-platform approaches by who draws the screen (platform toolkit vs framework renderer) and where logic runs (shipped runtime vs compiled). React Native, .NET MAUI and NativeScript top-left; Flutter and Ionic top-right; Compose Multiplatform bottom-right; Duet, two native apps, KMP shared-core practice, Rust cores and Skip bottom-left.

Who draws the screen × where the logic runs. The bottom-left cell is the share-the-logic route; the survey page walks every placement.

The best cross-platform frameworks in 2026 places every mainstream option on this grid, one comparison column per approach.

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 sharing does not cost: platform access. The shared core compiles into each app as an ordinary library; anything the platform can do, the app can still do, because the interface layer and the workers are native code. That includes the screens that must be native and fast — a Metal-drawn view, a camera pipeline, a video editor. In the share-the-logic route they are ordinary platform code, with the shared core deciding when they start and what they return; in the share-everything route each one is an escape through the framework’s native bridge, written per platform anyway in Swift and Kotlin that someone writes and the app build compiles. The survey page scores every approach on that escape.

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

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.
In shared-logic practice, the logic majority: Forbes states 80%+ shared logic for its KMP apps. In a Duet project, each feature’s state, actions, reducer, effects and test corpus are shared; the two view layers and the platform workers are not. The number depends on how interface-heavy the product is.
The shared core is Kotlin compiled into two ordinary native apps — a library, not a runtime. Both codebases open in Xcode and Android Studio and ship to the stores like any native app. That is different in kind from a shared-UI framework, whose widgets and runtime the app cannot ship without.
Yes, and it is cheaper if the iOS app records its behavior from day one. Duet’s iPhone-first flavor does exactly that: features are recorded and specced as they are built, so the later Kotlin core is written against an executable specification instead of reverse-engineered. The Duet overview covers the per-feature migration.
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 Modaalstarting a Duet project walks through the two template cards.

Sources and further reading

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