Rewriting a Native App in Flutter: Lessons from a Full Migration
What I learned rewriting a production native app as a single Flutter codebase — when a rewrite makes sense, how to plan it, and the mistakes that cost the most time.

At Shiftboard, I worked on rewriting an entire production app from separate native codebases to a single cross-platform Flutter app. The goal was a consistent experience across Android and iOS, less duplicated work, and cleaner communication with the backend.
Rewrites have a bad reputation, and often deservedly so. But done carefully, moving from native to Flutter can pay off quickly. Here’s what I learned, including the things I’d do differently.
First question: should you rewrite at all?
A rewrite is expensive. For weeks or months, you’re building features users already have. Before starting, I’d want clear answers to these:
- Are you maintaining two native codebases that do the same thing? This is the strongest argument for Flutter — every feature and bug fix currently costs double.
- Do the two platforms behave differently in ways users notice? Inconsistency between the Android and iOS apps is a common, underrated source of support tickets.
- Is the existing code a barrier to change? If small features take weeks because of accumulated complexity, a rewrite can reset that — if you architect the new app better.
- Does the app depend heavily on platform-specific features? Heavy use of native APIs doesn’t rule Flutter out (platform channels exist), but it increases the cost.
If you only have one native app and it’s healthy, adding Flutter to it (Flutter’s add-to-app support) is often a better option than replacing it.
Plan around feature parity, not screens
The mistake I see most often is treating a rewrite as “rebuild each screen”. Screens are the visible part; the behavior hides underneath. Before writing any Flutter code, it pays to build a feature inventory of the existing app:
- Every screen and every state it can be in (empty, error, loading, offline).
- Every API call, including retries, headers and error handling.
- Every piece of locally stored data and its format.
- Push notification types and what each one does when tapped.
- Deep links.
- Platform-specific behaviors (widgets, share extensions, background tasks).
That list becomes the definition of “done”. Without it, you ship the new app and discover through one-star reviews which edge cases the old app used to handle.
Don’t lose users’ data on upgrade
The Flutter app replaces the native app in place: same package name, same bundle ID, installed as an update. That means anything the native app stored locally — login tokens, settings, cached data, drafts — is still on the device, in native formats.
If you ignore it, every user gets logged out on update and loses their settings. That’s a terrible first impression for a rewrite.
Plan a migration step on first launch of the Flutter app:
- Read native
SharedPreferences(Android) andUserDefaults(iOS) values, either through a platform channel or packages that can access the same stores, and translate them. - Move auth tokens from the native keychain/keystore entries into whatever secure storage the Flutter app uses.
- Mark the migration as done so it runs exactly once.
Test this on devices that have the old app installed with real data, not on clean installs.
Clean up the API layer while you’re there
A rewrite is a rare chance to fix how the app talks to the backend. On this project, optimizing backend communication and API handling was an explicit goal. Things worth doing:
- Put one API client in charge of base URLs, auth headers, token refresh, timeouts and error mapping, instead of each screen building requests.
- Generate or strictly type the models, so a field renamed on the server fails loudly in development instead of silently in production.
- Remove redundant calls. Old apps accumulate “just to be safe” requests; a rewrite is the time to question each one.
This is also when I set up the architecture I want to live with for the next few years, rather than porting the old structure over.
Match platform expectations where they matter
Flutter lets you share almost all of your UI, but users on each platform have habits:
- Back navigation: Android users expect the system back gesture to work everywhere, including closing dialogs and bottom sheets. Check it on every screen.
- Scroll physics and page transitions: Flutter adapts these per platform by default — don’t override them without a reason.
- Date and time pickers, dialogs, switches: consider adaptive widgets (
Switch.adaptive,showAdaptiveDialog) where the native look matters to users. - Text scaling and accessibility: test with large system fonts. Native apps often handled this implicitly; a Flutter layout with fixed heights may not.
A consistent brand across platforms is good. Ignoring platform conventions is not.
Release gradually
Don’t flip 100% of users to the new app on day one. Both stores support staged releases:
- Google Play: staged rollout by percentage.
- App Store: phased release over seven days.
Start small, watch crash reports and reviews closely, and have a plan for pausing the rollout. Make sure crash reporting and analytics are in the Flutter app from the very first build, so you can compare stability with the old app.
What I’d do differently
- Write the feature inventory even earlier, and have the product owner sign off on it. It prevents a lot of “the old app used to do X” late in the project.
- Build the data migration first, not last. It’s the riskiest part and the hardest to test.
- Ship something small early. If there’s a self-contained part of the app, releasing it in Flutter first (even via add-to-app) validates the tooling, the pipeline and the team before the big switch.
Was it worth it?
For an app with two native codebases doing the same job, yes. After the migration, a feature was built once, tested once and behaved the same on both platforms. Bug reports stopped starting with “only on iPhone…” or “only on Android…”. And the cleaner architecture made the app faster to change for everyone who came after.
Takeaways
- Rewrite when you’re paying twice for everything; consider add-to-app otherwise.
- Define “done” with a full feature inventory, not a list of screens.
- Migrate users’ stored data and tokens on first launch — and test it with real old installs.
- Use the rewrite to fix your API layer and architecture.
- Respect platform conventions, and roll out gradually.
If you’re weighing a move from native to Flutter, I’m happy to talk it through.
Comments
Questions, corrections or your own experience — leave a comment below (GitHub sign-in).