Few technical decisions attract as many strong opinions as this one. Ask a room of developers whether a new app should be native or cross-platform, and you will hear about performance, about “feel”, about a framework that let someone down in 2019. What you will hear less often is the question that usually decides it: what does this particular app need?

For most products the honest answer is less dramatic than the debate suggests. Here is how we think about it.

What the choice actually is

Native means building each platform in its own language and toolkit: Swift and SwiftUI for iOS, Kotlin and Jetpack Compose for Android. Two codebases, each with direct access to everything its platform offers.

Cross-platform means one codebase that runs on both. The two frameworks most teams consider work differently. React Native, which we use, renders real platform components and lets you write native code where you need it. Flutter draws its own interface with its own rendering engine, so it looks the same everywhere unless you make it adapt.

There is also a third option that is easy to forget: the web. A well-built web app installed to the home screen covers a surprising number of cases, as long as it does not need deep access to the device.

Between those sits a fourth approach: share the logic, not the interface. Kotlin Multiplatform, for example, lets both apps use the same business rules and data handling while each draws its screens with its own platform’s toolkit. It suits teams that want a fully native interface without writing everything underneath it twice.

These are not quality tiers. Each is a different set of trade-offs, and the right one depends on the product.

Where cross-platform is the right default

A large share of apps are, underneath, forms, lists, detail screens, accounts and a connection to a server. The hard parts are the data, the flows and the words, and all of those are the same on both platforms.

For that kind of product, a single codebase has real advantages:

  • One team, one set of decisions. A feature is designed, built, tested and reviewed once.
  • Both platforms stay in step. No release where Android waits for iOS, or the reverse.
  • Shared logic with the web. With React Native, validation rules, data types and business logic can live in shared TypeScript packages that the web app uses too.
  • Lower cost to change. In the early life of a product, when everything is still moving, that matters more than almost anything else.

The platform components are still native, so a well-made React Native app scrolls, navigates and responds the way people expect on their phone.

Where native earns its cost

Native development becomes worth its extra cost when the product depends on something a cross-platform layer makes harder:

  • Heavy use of platform features. Widgets, watch apps, car displays, deep system integrations. They are possible from cross-platform code, but each one means native work anyway.
  • Demanding graphics or real-time processing. Games, augmented reality, audio and video processing, where every frame and every millisecond counts.
  • Being first to new operating system features. New capabilities arrive in the native toolkits first. If your product competes on having them on launch day, you want no layer in between.
  • Two different products. If the iOS and Android versions should behave differently in substance, not only in detail, a shared codebase stops saving much.

If none of these describes your product, native is mostly buying you two of everything.

When the web is enough

Before choosing between two kinds of app, it is worth asking whether you need an app at all. The web has quietly closed much of the gap. A web app can be added to the home screen, work offline, use the camera, and, on iPhones since iOS 16.4, receive push notifications once it has been installed.

What the web still does less well is the edges: long-running background work, deep integration with the operating system, some hardware access, and a presence in the app stores where many people still look first. If your product needs none of those, a web app reaches every device with one codebase and no store review, and every update reaches everyone the moment it ships.

For our own product, the public passport that a QR code opens is a web page, deliberately. A buyer standing next to a second-hand e-bike should not have to install anything to check its battery.

What people actually notice

People using your app do not know or care what it was built with. What they notice is concrete: how long it takes to open, whether scrolling stutters, whether text fields behave the way every other field on their phone does, whether the back gesture goes where they expect. A native app can get every one of those wrong, and a cross-platform app can get every one right. The framework sets the ceiling. The care taken inside it decides where the app actually lands.

Changing your mind is expensive

This decision is hard to reverse. Moving from cross-platform to native, or the other way, usually means rebuilding the interface layer from scratch, whatever the migration plan promises. That argues for choosing on the product you expect to have in two years, not the one you would like to have in ten.

It also argues for keeping the escape hatches open. React Native lets you write a single screen or module natively when you need to, so choosing cross-platform does not lock you out of native where it matters.

What we chose for our own app

The E-Bike Battery Passport’s mobile app is built with Expo and React Native, in the same TypeScript as the web portal it sits beside. It scans QR codes with the camera, keeps an encrypted cache so a workshop can open records without a connection, and receives push notifications. Every one of those is a native capability, and all of them work from the shared codebase through native modules.

The native parts do not disappear

Cross-platform does not mean never touching the platform. The app’s encrypted offline cache depends on a native database module, and that is why the app does not run in a web browser, even though Expo can build for the web. A build that lacks the module has to find out before it tries to use it, not by crashing:

import { requireOptionalNativeModule } from 'expo';

// Ask whether this build carries the native database before loading it.
// Importing it on a build without it would fail at load time; this returns null.
async function loadSqlite() {
  if (!requireOptionalNativeModule('ExpoSQLite')) return null;
  return import('expo-sqlite');
}

Checks like this are ordinary in a cross-platform app. They are the seams where one codebase meets two platforms, and they deserve the same care as any other boundary.

Devices still have the last word

Automated tests that render screens can tell you what a screen shows and what a tap changes. They cannot see the camera preview, the keystore, a permission prompt or how a layout sits on a real display. That still takes a real phone in a real hand, for every change that touches the device. One codebase halves the code. It does not halve the testing.

One codebase, two platforms, different costs

Shipping to both stores involves more than code. Each platform has its own developer programme, review process, signing and test distribution. Android and iOS can arrive on different schedules for reasons that have nothing to do with the app itself. For our own app, Android came first, and iOS follows once the Apple side of distribution is in place. A cross-platform codebase makes that second platform much cheaper, but not free.

The trade-offs side by side

ConsiderationCross-platformNative
CodebaseOne, shared, often with the webOne per platform
TeamOne team for both platformsPlatform specialists, or twice the work
Look and behaviourNative components with React Native; drawn by FlutterExactly the platform’s own
Device featuresThrough modules, with native code where neededDirect, and first to new ones
PerformanceEnough for the large majority of appsHeadroom for graphics and real-time work
Cost of changeOne change, both platformsEvery change made twice

Questions to ask before choosing

Rather than starting from a technology, start from the product:

  1. What does the app need from the device? List the capabilities: camera, location, background work, storage, notifications. Check each against the framework you are considering.
  2. How different should the two versions be? If the answer is “in small details only”, share the code.
  3. Is there a web product beside it? Shared types and logic across web and mobile are one of the strongest arguments for React Native.
  4. Who maintains it in two years? A codebase the team can keep healthy beats a theoretically better one it cannot.
  5. What does being late cost? If a new operating system feature on day one matters to your users, that points to native.

The answer is usually boring

For most business apps, the sensible default is cross-platform, with native code at the few points where the product needs the device directly. For products whose value is the device experience itself, native pays for itself. The mistake is choosing either one before knowing which kind of product you are building.

The best choice is the one you stop thinking about once it is made, because the app works, the team can change it, and the people using it never wonder what it was built with.