FlutterFlow gets a lot of products to their first hundred users. Then someone wants a feature the visual builder can't express, or a second developer joins and finds they're editing the same pages in a browser, or the team wants real code review. A FlutterFlow to Flutter migration is the usual answer. The good news is you aren't starting from nothing: FlutterFlow exports real Flutter code. The less good news is that the export was written for a generator to regenerate, not for people to maintain. This guide covers how the export works, what we'd keep, and what we'd rewrite.
What FlutterFlow actually exports
FlutterFlow gives you three ways out: download the code as a zip, push to a GitHub repository, or pull it with the FlutterFlow CLI. Code export is tied to your subscription, so check which plan includes it before you plan around it.
The GitHub route matters for migration planning. Per FlutterFlow's docs, it always pushes to a branch named flutterflow, and anything you change on that branch is overwritten by the next push. Their suggested workflow is to branch a develop from it and merge FlutterFlow's pushes in through pull requests. And sync is one-way. Changes you make in GitHub do not go back into the visual editor. The moment your team edits exported code by hand, you've got two sources of truth, and every new push from FlutterFlow becomes a merge you have to reason about.
The exported project has a recognisable shape:
lib/flutter_flow/: generated utilities, theme, navigation, helper widgets and the custom functions file.lib/custom_code/: your custom actions and widgets, the code you wrote inside FlutterFlow.lib/backend/: API calls (api_requests), cloud function wrappers and Firestore schema classes if you used Firebase.app_state.dart:FFAppState, a singleton that extendsChangeNotifierand holds every App State variable you defined.- One folder per page, with the page widget and its model class.
It compiles, it runs, and it's yours. That's a better starting point than a lot of hand-written codebases we've been handed.
Planning a FlutterFlow to Flutter migration: keep or rewrite
The mistake to avoid is a big-bang rewrite. You already have a working app with users. Treat the export as the first commit of a normal Flutter project and replace parts in order of how much they hurt.
Usually worth keeping (at least at first)
- Your custom code. Custom actions and widgets were written by a person, often to work around something. Read them, add tests, move them into the new structure.
- Firestore schema classes and API wrappers, as a reference. They document the shape of your data. You'll probably replace them, but they tell you what the backend expects.
- Static screens like onboarding, settings and about pages. They work. Rewriting them buys you nothing until you need to change them.
- Theme values. Colours, font sizes and spacing from the generated theme map straight onto a
ThemeDataand a small set of design tokens.
Usually worth rewriting
- Global state in
FFAppState. OneChangeNotifierholding everything means any change can rebuild widgets that don't care about it, and nothing tells you which screen owns which value. Split it by feature into whatever state management your team will maintain (BLoC, Riverpod, plainChangeNotifierper feature). We went through a similar exercise moving an app from GetX to BLoC and wrote it up in our GetX to BLoC migration post. The same "one feature at a time behind a stable interface" approach applies. - Data access inside widgets. Generated pages often query Firestore or call APIs straight from the build tree. Pull that into repositories so you can test it, cache it, and change the backend without touching UI.
- Navigation once you need deep links, auth guards or nested tabs that the generated router fights you on.
- Deeply nested layouts. Generated widget trees are long. A screen that's 900 lines because every padding is its own widget is a screen nobody wants to edit. Extract components when you touch them.
- The generated helper widgets in
flutter_flow/. Replace them with standard Flutter or your own components as you rewrite each screen, then delete the folder when nothing imports it.
A migration order that keeps the app shippable
- Freeze the visual editor. Pick a date after which nobody edits in FlutterFlow. Export one last time. Two sources of truth is the expensive state, so keep it short.
- Get the project building in CI with pinned Flutter and package versions. Turn on
flutter analyzeand accept that the first run will be noisy. Fix errors, log warnings, move on. - Write smoke tests for the money paths: sign-up, login, the core action, payment if you have one. These protect you during everything that follows.
- Introduce your architecture next to the old code. New folders for features, a repository layer, your state management choice. Don't move anything yet.
- Migrate one feature at a time, starting with the one you need to change next for product reasons. Move its state out of
FFAppState, move its data calls into a repository, rebuild its screens, delete the old files. - Delete
flutter_flow/andFFAppStatewhen nothing references them. That's your finish line.
Releases keep going throughout. Each step should be small enough to ship on its own.
What to check before you commit
- Package versions. Exports pin whatever FlutterFlow used at the time. Upgrading Flutter and packages is often the first real piece of work, and it can surface deprecated APIs across the codebase.
- Firebase config and secrets. Check that API keys, service config and any hardcoded endpoints are where you expect, and that nothing sensitive is committed that shouldn't be.
- Who owns the backend. If your app depends on FlutterFlow-managed integrations, make sure you can run them without the visual editor before you freeze it.
- Your team's skills. A migration only pays off if someone can maintain hand-written Flutter afterwards. If nobody can, staying on FlutterFlow longer is a valid choice.
We haven't published a FlutterFlow migration case study. This guide comes from working in large Flutter codebases (Crumb Count is over 82,000 lines across 13 feature modules) and from state management migrations on client apps. If you want someone to read your export and give you a keep or rewrite list before you commit to anything, that's the kind of short engagement our technical consulting work covers.