Your App Still Works. That's Not the Same as Being Maintainable.

Aranda Morrison

Legacy apps don't fail in obvious ways. They keep opening, users keep logging in, and the dashboard stays green. The troubles start when you need to make a change. A new Xcode release breaks the build. Google Play raises its target API level. A dependency gets a CVE and the patched version needs a framework upgrade you can't do. At that point the app is still running, but you can't safely update it.
The technologies ageing worst
Xamarin is the clearest case. Microsoft ended support on 1 May 2024, so it no longer gets platform or security updates. .NET MAUI is the official successor, but migrating to it is closer to a port than an upgrade, so it's worth comparing like for like before choosing it by default.
Cordova and older Ionic apps (especially Ionic 1–3 on AngularJS) have a different problem. They rely on a web view plus a plugin ecosystem, and many of those plugins are no longer maintained. Each iOS and Android release tends to break another native bridge. Performance and accessibility also fall further behind native apps every year.
First-generation native apps age too. Objective-C with UIKit and storyboards, or Java with XML layouts, AsyncTask and Kotlin synthetics, still compile. But they’re no longer the frameworks Apple and Google are investing in: SwiftUI, Swift Concurrency, Jetpack Compose and coroutines. Hiring for these codebases gets harder, and new platform features increasingly assume the modern stack.
Early React Native apps built on the legacy bridge architecture, with years of unmaintained community modules, can be as fragile as any of the above. Upgrading across many React Native versions can be devilishly hard.
What's changed in our recommendation
For years our advice was simple: Flutter or React Native, depending on your team and the product's requirements. One codebase, two platforms, and roughly half the maintenance cost of two native apps.
We still recommend both. What's changed is that going fully native, with SwiftUI on iOS and Kotlin with Jetpack Compose on Android, is back on the table. The case against native was always economic: every feature built twice, every bug fixed twice, and two codebases drifting apart. Spec-driven AI development removes most of that cost, because both apps are built and verified against the same executable specification.
Spec-driven, with humans in the loop
In our process, the source of truth is an executable specification, not any one codebase. Requirements are written as Given/When/Then scenarios that drive acceptance tests on every platform:
Coding agents are fast, but left alone they drift. They lose track of the goal, invent requirements, and "fix" failing tests by weakening them. So we constrain them:
Specs come first and are protected. Stakeholders review and approve them, and agents cannot change them.
An architecture guide sets the boundaries. It covers folder structure, state management rules, domain language and approved libraries, so the generated code looks like one team wrote it.
Tests are the fitness function. Agents write and maintain unit tests under the acceptance layer, and refactor when quality indicators slip.
Humans own judgement. Engineers review every change, and QA tests manually before release.
For a native build, agents implement each scenario in SwiftUI and in Compose, and a feature isn't done until its acceptance tests pass on both. Divergence between platforms shows up as a failing test, not a surprise in production.
We used this approach on Independent Living Australia's Keep Able On The Go app, which moved two native apps into one React Native codebase. Because the goal was feature parity, AI extracted the initial Gherkin requirements (the Given/When/Thens) from the existing iOS and Android code, and stakeholders then refined them. A project-specific pipeline turned those specs into acceptance tests, with step bindings tied to the exact spec text. Complex offline behaviour, including local playback, locally scheduled alerts, and view history that syncs on reconnection, was covered by executable scenarios. The specs now serve as living documentation that grows with the app.
Where to migrate to
There are three targets we recommend today. The right one depends on the app you have, the team that will maintain it, and how long the product needs to last.
Native: SwiftUI and Kotlin with Jetpack Compose. This is the best fit when platform fidelity matters: deep OS integration, Bluetooth and hardware features, accessibility, widgets, or getting new Apple and Google APIs as soon as they ship. It's also the natural path for aging Objective-C/UIKit or Java/XML apps, because the platform languages and tooling stay familiar even though the UI layer changes completely. With spec-driven development keeping both apps in sync against the same acceptance tests, running two codebases costs far less than it used to.
Flutter. A strong choice for products where a consistent, custom-branded UI across both platforms matters more than matching native platform conventions. Flutter draws its own UI instead of wrapping native components, so look and behaviour are predictable across devices and OS versions. It's also a common destination for Xamarin and Cordova apps whose owners want one codebase without depending on a web view.
React Native. The right fit when your organisation already works in TypeScript and React, or wants to share logic and skills with a web product. Current React Native, built on the New Architecture, is a big step up from the bridge-based versions many older apps still run. It's often the most practical path for Ionic and Cordova apps, since the team's web skills carry over. This is the target we chose for Keep Able On The Go.
The choice isn't about which framework is fashionable. It's about which one your organisation can keep secure, updated and improving for the next five years or more.
Not sure whether your app needs a repair, a migration or a rebuild? Request an App Review or read more about our mobile app modernisation service.



