FlutterFlow Custom Code: When to Drop to Pure Flutter in 2026
FlutterFlow's custom code limits are not where most people expect them. The builder will happily let you paste Dart into a Custom Action all afternoon. What it will not do is read that Dart the way the Dart analyzer reads it.
I have shipped eight templates and more than 40 custom widgets on the official FlutterFlow Marketplace, which mostly means I have spent a lot of time sitting on the exact seam where the visual builder stops and real Flutter starts. That seam is sharper than the marketing suggests and softer than the "it's just a toy" crowd claims.
So: what the limits actually are, why the ceiling is priced before it is technical, the four signals that mean you should leave, and the export gotcha that eats a weekend.
What Are FlutterFlow's Custom Code Limits, Exactly?

FlutterFlow's real limit is its Dart parser, not the volume of code you write, and anything the parser cannot model gets dropped without an error. The builder reads your files so it can expose your types back into the visual editor. Anything it fails to read simply does not appear.
Three kinds of custom code exist, and they are not interchangeable. This table is the thing I wish someone had put in front of me on day one.
| Type | Can import pub.dev packages | Returns a value | What it's for |
|---|---|---|---|
| Custom Function | No | Yes | Synchronous transforms, formatters, validators |
| Custom Action | Yes | Yes | Async work, SDK calls, API glue |
| Custom Widget | Yes | No | Anything the widget tree can't express |
Per FlutterFlow's custom code documentation, Custom Functions cannot import new files or packages beyond the default set, while Custom Actions and Custom Widgets can pull in pub.dev dependencies. That one row explains most of the "why won't my function compile" threads. If your helper needs a package, it is not a function. It is an action.
The parser rules are stricter still. FlutterFlow's Code File docs state that classes with generic types such as class ApiResponse<T> {} are not supported, that methods or fields with function types as parameters or fields are ignored, and that Dart extensions are not supported yet. Those three lines rule out a wide slice of ordinary Dart. No generic result wrapper. No onTap callback field on a model. No extension StringX on String.
Actually, that overstates it. You can still write all of those inside a Custom Action's body, where the parser only cares about the signature. What you cannot do is hand those shapes back to the builder as first-class types it understands.
The Ceiling Is a Pricing Ladder Before It Is a Technical One

The first wall most teams hit is a billing page, not a compiler error. FlutterFlow gates its escape hatches by plan, and the gates happen to sit exactly where a growing project needs them.
Read the tiers on FlutterFlow's 2026 pricing page in order. Free is $0 and cannot download source at all. Basic at $39/month adds project source code and APK download. Growth at $80/month for the first seat turns on Code Extensibility, Custom Code Expressions and YAML editing. Business at $150/month for the first seat is where Custom Classes and CLI access live.
Now look at what that sequence implies. The capabilities you reach for on the day the visual editor stops being enough (importable Dart files and scripted exports) are also the most expensive ones. A solo builder on Basic who needs custom classes is being asked to go from $39 to $150 a month, nearly 4x, to keep using a tool they have already outgrown in one specific direction.
There is nothing scandalous about that; it is an ordinary SaaS ladder. It does change the question though, from "can FlutterFlow do this" to "at what monthly price, and is plain Flutter cheaper at that price". For a one-developer project the answer flips earlier than most comparison posts admit. I worked through the fuller trade in FlutterFlow vs Flutter for production apps.
When Should You Drop to Pure Flutter?

Drop to pure Flutter when two of these are true at once: graph-shaped state, generic models, a native SDK dependency, or tests that must run in CI.
Any one signal on its own is usually solvable inside the builder. Two of them compounding is where the workarounds start costing more than the move would.
The concrete version of each:
- Your state is graph-shaped. App State plus page state covers screens that read and write a handful of values. Once a single change has to invalidate four unrelated views, you want Riverpod or Bloc holding that graph rather than a pile of Custom Actions poking at
FFAppState. Note that reading and writing app state from custom code already means importingflutter_flow_util.dartand usingFFAppStatedirectly, per FlutterFlow's custom code examples — so you are half outside the builder before you even decide to leave. - Your models need generics. A
Result<T>or a paginatedPage<T>is the ordinary way to model an API layer in Dart. The parser will not read it. - A native SDK sits in the critical path. Anything that needs custom Podfile edits, entitlements, or a hand-written platform channel is fighting the export rather than the builder, and that fight does not get easier with project size.
- Tests are a requirement, not a wish. Widget and integration tests against generated code are possible. They are just miserable to keep green when the next re-export rewrites the file underneath them.
Have you ever opened the exported project "just to check something" and still been in it three days later? That is the signal arriving early.
The Export Is Not the Hard Part
Code export works. Basic and up hands you the whole Flutter project, it compiles, and it reads like Flutter. The hard part is the second export, and the third. Everything painful about leaving comes from the window where the builder and your editor both believe they own the same file.
How Do You Move Off FlutterFlow Without a Rewrite?
Move in stages: shrink FlutterFlow's surface area from inside the builder first, then export once and stop re-exporting, so ownership never splits. The migration that fails is the one that keeps both sides authoritative.
- Push every non-trivial behaviour into Custom Actions and Custom Widgets while you are still in the builder. People skip this step because it feels like doing the work twice. It isn't. Do it and the exported project already contains your logic, in files you wrote, with names you chose.
- Move business rules to the backend. Cloud Functions, or a real API, whatever you already run. Server-side logic survives any client rewrite, which is the same reason I keep subscription state off the client on every project I ship.
- Export with the CLI and commit the result untouched. That gives you a diffable baseline.
- Make one small change in the builder, re-export, and read the diff. That diff is an exact map of which files FlutterFlow believes it owns.
- Cut over screen by screen, replacing generated pages with hand-written ones behind your existing router. Nothing about this forces a big-bang switch.
If you are still choosing a direction, the alternatives to FlutterFlow worth considering are a better starting point than another feature grid, and the Flutter developer tools I actually keep installed describe what the post-export workflow looks like day to day. If the vocabulary here is new, start with what Flutter is and how it differs from a wrapper.
The Gotcha That Costs People a Weekend
.flutterflowignore does not live where you think it lives. It goes in the parent of your exported project folder, not inside it.
FlutterFlow's export documentation spells it out: if the project sits at /Users/you/projects/my_flutterflow_app/, the ignore file belongs in /Users/you/projects/, and every entry inside it carries the project folder as a prefix, like my_flutterflow_app/ios/Runner/Info.plist. Write it the way you write a .gitignore and it matches nothing at all, quietly, and the next export flattens the native config you spent an evening getting right.
I learned that one on a Sunday, three exports deep, watching git status list ios/Runner/Info.plist as modified for the third time and assuming Xcode had touched it. The same shape of problem hits pubspec.yaml. Add a dependency by hand, export, and it is gone unless the builder knows about it too.
Where I'd Draw the Line
If you are pre-launch and the app is mostly screens over a Firebase collection, stay in the builder and stop reading comparison posts. If you have paying users and more than roughly a dozen Custom Actions, export, commit, and start treating FlutterFlow as a design tool that happens to emit Dart.
Open your project and count your Custom Actions right now. What number did you get?
