Why We Build Native: Swift, Kotlin, and the End of the "Build It Twice" Excuse
We build iOS in native Swift and Android in native Kotlin. Two real apps, each speaking its own platform's language. We do it for one reason above all others: maintainability. A native app has a dramatically longer shelf life than an app built on a cross-platform shortcut. The shortcut feels cheaper in month one. It's almost always more expensive by year two — and by year three it's often a rewrite.
The usual objection to native is "but now you have to build everything twice." That objection used to have teeth. It no longer does — and the reason why is new enough that most people haven't caught up to it yet. More on that at the end.
This isn't fashion. I've been shipping mobile apps since the App Store opened, and I've watched every "write once, run everywhere" promise arrive, get adopted, and quietly become a liability. Native has outlasted all of them.
A pattern I've watched for fifteen years
Every few years a new framework promises to let you write your app once and ship it to both platforms. Each one is genuinely better than the last. And each one, at scale and over time, has cost more than it saved.
- PhoneGap / Cordova (early 2010s) — wrap a web page in a native shell. Fine for brochure-ware, fell apart the moment you needed real device features or smooth performance.
- Flutter (Google's bet) — a whole new language (Dart) and rendering engine. Beautiful demos. Brutal upgrades.
- React Native (Meta's bet) — write in JavaScript/TypeScript, run through a bridge to native code. The best of the bunch, and still a maintenance treadmill.
The marketing always emphasizes the start of the project. The bill always comes due in the middle — the upgrade, the new OS version, the one SDK you have to add that the framework doesn't support cleanly. None of these tools has ever, in my experience, paid off on long-term viability. And that's the metric that actually matters, because an app isn't a thing you launch — it's a thing you own for years.
What this actually costs — three apps from our own portfolio
I'm not arguing from theory. These are apps we own and maintain.
Flutter: our fitness apps, frozen in 2020
Our FitHer app and its sibling Flutter fitness apps are stuck in time. The project targets a Dart SDK range of >=2.8.0 <3.4.0 — the pre-null-safety era of the language — and leans on Firebase packages from 2020 (firebase_core: ^0.5.3, firebase_auth: ^0.18.4+1), three-plus major versions behind today.
The thing that broke the camel's back was advertising. Adding or upgrading a simple AdMob package meant dragging that entire 2020-era Flutter and Firebase stack forward all at once — because in Flutter everything is coupled through the framework and its plugin ecosystem. Our companion tracker app is frozen on google_mobile_ads: ^2.4.0 while the current line is 5.x, and it only stays standing thanks to a dependency_overrides hack papering over version conflicts. Both apps have effectively been frozen since mid-2024, because moving anything means moving everything.
We're not upgrading FitHer. We're converting it to native SwiftUI. That tells you what we actually think the asset is worth in Flutter form.
React Native: the advertising never quite worked
One of our larger apps is React Native, and it's the clearest case study I have for why the architecture itself — not the team — is the problem.
It's a big app: 48,467 lines of TypeScript/JavaScript across 484 files, 131 dependencies, on a current React Native and Expo. The team is good. The framework still fights them constantly, and advertising is where it shows worst — which matters, because ads are how these apps make money:
- Android crashes from running out of memory — the ad SDKs blow past Android's default memory ceiling on Samsung and Xiaomi devices. The fix was a manual
largeHeapworkaround, not anything the framework handled for us. - A native crash inside the AdMob WebView, caused by a race condition between JavaScript and native code — exactly the kind of timing bug the JS-to-native bridge creates and that you can't fully design away.
- An iOS crash after interstitial ads, caused by the renderer trying to navigate before its own state updates finished.
- Rewarded ads getting stuck, requiring a two-minute safety timeout to force them to complete.
The team keeps a PINNED_DEPENDENCIES.md file with a flat warning: many packages are pinned to exact versions and must not be upgraded without checking it first. When you can't safely update your own dependencies, you don't own a codebase — you own a museum exhibit. Every ad bug means diving through four languages and an async bridge to fix something that, in native code, is a handful of lines with synchronous, predictable behavior.
React Native → native: PrayAI
PrayAI came to us on React Native + AWS Amplify: 4 repositories, 252 npm dependencies, and 35,934 lines of infrastructure code supporting 12,900 lines of actual product. Every major dependency was 2–4 versions behind, with an upgrade bill estimated at $48,000–$78,000 that grew every month we didn't pay it.
We rebuilt it. It now runs as a single Rails application with thin native Swift and native Kotlin shells (Hotwire Native) — infrastructure dropped to 184 lines, hosting from ~$2,250/month to ~$50, and a feature ships to web, iOS, and Android in one deploy.
I'm including PrayAI deliberately, because it shows the other right answer. When an app is mostly content and forms, a Rails backend with thin native shells is the better native strategy. When an app is interaction-heavy — gestures, real-time, deep device integration, heavy monetization — full twin-native Swift and Kotlin is the better one. The wrong answer in both cases is a cross-platform JavaScript or Dart framework. Native, in one form or the other, is what we reach for.
"But now you maintain two codebases"
This is the one real cost of going native, and it's the objection every cross-platform pitch is built on. Here's why it no longer outweighs the benefits.
The promised code sharing rarely materializes anyway. PrayAI's React Native build had a "shared UI components library" that contained exactly one component. Cross-platform apps end up riddled with if iOS / else Android branches because the two platforms genuinely behave differently — different date pickers, different memory models, different ad SDK behavior. You don't escape the two-platform problem. You pay for it twice and add a layer on top.
Two clean native codebases are easier to maintain than one tangled cross-platform one. Swift code reads like iOS. Kotlin code reads like Android. When something breaks, the stack trace points at the actual problem — not at minified JavaScript three abstraction layers down. New developers are productive in weeks, because they only need to know the platform, not the platform and a framework and a bridge and a build tool that hides all three.
And here's the part that's genuinely new: AI agents close the parity gap. The reason "build it twice" was expensive was the human coordination — making sure a feature added to iOS also lands on Android, the same way, on the same schedule. That's now a job I hand to a coding agent. I describe the feature once; the agent implements it in Swift, implements it in Kotlin, and aligns the two to match. The codebases stay in lockstep without a human manually mirroring every change. The "twice the work" math that justified cross-platform frameworks simply doesn't hold the way it did even two years ago.
So we get native performance, native debugging, native longevity, and day-one access to every new OS feature — and we no longer pay the historical penalty for it.
What you get by choosing native
- A longer shelf life. Apple and Google support Swift and Kotlin as first-class citizens with clear, incremental upgrade paths. Your app ages gracefully instead of hitting a cliff.
- No framework upgrade treadmill. You're never one major framework release away from a five-figure migration just to keep the lights on.
- Day-one access to new platform features. When Apple or Google ships something new, native apps use it immediately — no waiting for a framework to catch up or hoping a plugin author maintains the wrapper.
- Monetization that actually works. Ad SDKs, in-app purchases, and subscriptions are built for native first. As our own apps show, this is exactly where cross-platform frameworks hurt the most.
- Performance and stability your users feel. No JavaScript bridge, no extra rendering engine. Smooth scrolling, fast launches, fewer crashes.
- Talent you can actually hire. Every mobile developer knows the native platforms. You're not hunting for the rare specialist who knows your specific framework-of-frameworks combination.
The bottom line
We build native because we optimize for the years you'll own the app, not the weeks it takes to launch it. Every cross-platform shortcut I've watched over fifteen years — PhoneGap, Cordova, Flutter, React Native — traded a small early saving for a large later cost, and the bill always came due at the worst possible time: the upgrade you can't avoid, the ad SDK that won't cooperate, the OS release that breaks the framework.
The single reason native used to lose — "you have to build it twice" — has been dissolved by AI agents that keep a Swift app and a Kotlin app aligned for us. So we get the durable, performant, hireable, monetizable option and we get it without the historical penalty.
That's the bet. It's why your iOS app is Swift and your Android app is Kotlin.