Migrating a Native App to Flutter or React Native: Strategies
Advanced4 minInnovialabs editorial team
In short
A gradual migration is usually safer: new screens are built in Flutter or React Native inside the native app, one layer owns auth and data, and releases continue as usual; a full rewrite makes sense for small or badly outdated apps.
Two paths and how to choose
There are two main ways to move a native app to a cross-platform stack:
- Full rewrite: a new app is built from scratch and replaces the old one in a single release.
- Gradual migration (add-to-app, brownfield): Flutter or React Native is embedded in the existing native app, and screens move over one at a time.
| Criterion | Full rewrite | Gradual migration |
|---|---|---|
| Risk for current users | High: everything changes at once | Lower: changes ship in portions |
| Releases during the work | Old app is frozen or maintained in parallel | Continue as usual |
| Architecture complexity | Lower: one stack | Higher: two stacks at once |
| Build size during transition | Normal | Larger: two runtimes |
| Fits | Small apps, outdated code | Large, live products |
If the app is small and the code is hard to maintain, a rewrite can be simpler. If the product is large, earns money and keeps evolving, going gradually is almost always the wiser choice.
How a gradual migration works
- Flutter supports add-to-app: a Flutter module is added to the iOS and Android projects, and screens are opened via
FlutterViewControllerandFlutterActivityorFlutterFragment. Pre-warm and reuse the engine, otherwise the first transition to a Flutter screen is noticeably slower. - React Native is embedded into the existing app as a set of views that native code opens like ordinary screens.
Order of work:
- Define boundaries: which screens and flows move first. Good candidates are new features and isolated sections (profile, settings, catalog).
- Set up builds and CI so the module is built together with the native projects.
- Define the exchange contract between native and cross-platform parts: navigation, events, data.
- Move section by section, deleting native code after each one.
- When only a native shell is left, decide whether to switch to a fully cross-platform project.
How to keep releases going
- Put every migrated screen behind a feature flag and roll it out gradually.
- Keep the same bundle ID and signing: users should get a normal update, not a new app.
- Keep the old implementation until the new one proves stable in crash and product metrics.
- Plan so that each release changes only a limited part of the product.
Sharing auth and data
The most common problem is two parts of the app disagreeing about who the user is.
- One session owner. Tokens are stored in one place (Keychain, Keystore) and refreshed by one layer. The other part gets the token through a bridge instead of running its own refresh logic.
- A bridge with an explicit contract. In Flutter, platform channels; the Pigeon generator helps with typed interfaces. In React Native, native modules. Document the contract like an API: methods, types, errors.
- One source of data. If the native part uses a local database, the cross-platform part reads it through the bridge or a shared API rather than keeping its own copy.
- Events. Logout, language changes and profile updates must reach both parts.
// Flutter: request a token from the native side
const channel = MethodChannel('app/auth');
final token = await channel.invokeMethod<String>('getAccessToken');
Risks
- Transition performance between native and cross-platform screens.
- Larger build size during the transition.
- Two skill sets needed in the team at the same time.
- Native dependencies: SDKs without a plugin or module must be wrapped by hand.
- A stalled migration, where half the app stays on two stacks forever.
When migration is not worth it
- Native teams work well, and the product's problems are not about technology.
- The app depends heavily on platform features: advanced camera work, AR, background services, widgets.
- There are no resources to finish the migration. A half-done migration costs more than either clean option.
- The reason is fashion rather than measurable goals such as feature velocity, a single team or shared code.