I have now led two full migrations from React Native to Flutter on apps that people depend on every day: the Mengantar logistics platform and the SRX Property app in Singapore. Both shipped. Both kept their users. Neither was painless. This is the playbook I wish someone had handed me before the first one.
Why migrate at all
Nobody should rewrite a working app for fun. In both cases the trigger was the same combination: a React Native codebase that had drifted across several major versions, native modules nobody wanted to touch, and a team that was already comfortable with Dart. The business case was not "Flutter is faster". It was "we spend 30% of every sprint fighting the toolchain, and that number is growing".
If you cannot write down the cost you are paying today in a sentence a product manager understands, do not migrate yet.
Decide the shape before the first line of code
The biggest mistake I see is starting the Flutter project as a blank flutter create and porting screens one by one in whatever order the backlog suggests. Three months later you have a second app that is 60% done and a first app that still needs bug fixes.
Instead, spend the first two weeks on three things:
- Inventory every screen, every API call, and every native capability. Push notifications, deep links, camera, background location, payments. Native capabilities are where migrations die, so list them first.
- Settle the architecture. I use a modular Clean Architecture: one package per feature, a shared
corepackage for networking and DI, and a thinapppackage that wires routes. The structure is boring on purpose. Boring scales. - Pick the state management once. BLoC for anything with real business logic, Cubit for simple screens. Do not let each feature choose its own.
Port the plumbing first, screens second
The order that worked on both projects:
- Networking layer, auth, token refresh, and error mapping.
- Analytics and crash reporting. You want Crashlytics and your analytics stack live from the first internal build, not the last.
- Deep links and push notifications, because they touch every platform config file.
- Then the single most used screen in the app.
By the time the first real screen exists, the hard parts are already proven on both platforms.
Keep the old app shipping
A migration that freezes the current app for six months is a migration that gets cancelled. On Mengantar we kept a small rotation on the React Native app for bug fixes and critical features while the rest of the team built the Flutter version. Every feature that landed in the old app went into a shared checklist so it would not be forgotten in the new one.
Release behind a rollout, not a big bang
Both apps went out as a new build under the same package name, rolled out in stages on Google Play and through phased release on the App Store. Watch three numbers for the first week: crash-free rate, session length, and the conversion of whatever your core action is. If any of them drops below the old app, pause the rollout and fix it. A paused rollout is cheap. A one-star review wave is not.
What I would do differently
- Snapshot-test the old UI. We eyeballed parity. Screenshots of every old screen, organised by feature, would have saved days of "wait, was there a button there?"
- Migrate analytics event names verbatim. We "cleaned up" event names during the move, and the data team spent weeks stitching dashboards back together.
- Write the scaffolding tool earlier. The Rust CLI I built later to generate feature modules would have paid for itself during the migration.
A migration is a product project with an engineering deliverable, not the other way round. Treat it that way and it ships.