AI coding for Flutter: a verification-first workflow
What is AI coding for Flutter?
AI coding for Flutter means using a coding agent to inspect, edit, and verify a Dart and Flutter repository under explicit project constraints. The useful unit is not generated code alone; it is a reviewed diff that follows the repository's package versions, generated-file boundaries, architecture, and platform requirements and passes the appropriate analyzer, tests, and device checks.
TL;DR
- -Make the agent read pubspec.yaml, pubspec.lock, analysis_options.yaml, nearby implementation, and tests before it proposes code. Do not ask it to recall the current Flutter or package API.
- -Mark source files and generated outputs. Edit annotations, models, ARB resources, or other inputs; regenerate outputs with the project's existing command.
- -For async callbacks, verify BuildContext.mounted after every relevant async gap. For Riverpod, use lifecycle APIs supported by the version actually locked in the project.
- -A compiling widget is not a verified UI. Check local constraints, long translations, text scaling, keyboard insets, orientation, and supported form factors.
- -Use a narrow loop: format, analyze, targeted tests, then broader tests and a real-device or emulator flow when platform behavior is involved.
- -The official Dart and Flutter MCP server can expose analysis, tests, package search, runtime errors, and widget-tree inspection, but it is experimental and does not replace review.
An AI coding agent can write valid Dart and still break the app.
The failure is rarely “the model does not know Flutter.” More often it used the wrong package version, edited a generated artifact, crossed an async lifecycle boundary, or stopped after compilation without exercising the UI.
The remedy is not a longer prompt. It is a tighter engineering loop: inspect the repository, define the allowed change, generate a small diff, and verify the boundary that changed.
This is the workflow I use on Flutter projects such as JourneyBay. It applies to Claude Code, Codex, Gemini CLI, Cursor, and similar agents.
1. Start from the repository, not model memory
Before proposing an implementation, the agent should read:
pubspec.yaml
pubspec.lock
analysis_options.yaml
README or project instructions
the nearest similar implementation
the relevant tests
routing and state-management setup
build.yaml and l10n.yaml, if present
pubspec.lock matters. “Use the current Riverpod API” is ambiguous; “use the API supported by the locked flutter_riverpod version and copy the local provider pattern” is testable.
The same rule applies to GoRouter, Freezed, JSON serialization, platform plugins, and the Flutter SDK. Do not let the agent upgrade a dependency to make remembered syntax compile. A dependency change is a separate decision with its own review.
A useful first prompt is deliberately read-only:
Inspect the listed files and explain:
1. the existing architecture and state-management pattern;
2. package and SDK constraints relevant to this change;
3. generated files and their source inputs;
4. the narrowest tests and commands that verify the change.
Do not edit files. Do not add or upgrade dependencies.
List assumptions that the repository does not answer.
Approve the plan before asking for code.
2. Give the agent a bounded change contract
“Add favorites” is not a contract. State what the user can observe and which layers may change.
Goal:
A signed-in user can remove a saved place from the details screen.
Observable behavior:
- tapping Remove asks for confirmation;
- confirmation calls the existing repository once;
- success closes the dialog and updates the visible state;
- failure keeps the screen open and shows the existing error component.
Allowed files:
- place_details_page.dart
- saved_places_controller.dart
- their existing test files
Do not:
- change routes or public repository interfaces;
- edit generated files;
- add packages;
- modify Android or iOS configuration.
Verification:
flutter analyze
flutter test test/features/saved_places/
This catches scope creep early. The agent knows where it may work, what not to “clean up,” and how completion will be judged.
For a larger feature, split work by an observable slice rather than asking for domain, data, state, UI, localization, and platform configuration in one pass. The same small-cycle discipline is described in TDD with coding agents.
3. Treat generated code as output
Flutter repositories commonly generate Dart from annotations and resource files:
Freezed model → *.freezed.dart
json_serializable → *.g.dart
Riverpod annotation → *.g.dart
Injectable config → *.config.dart
ARB resources → AppLocalizations output
The exact mapping belongs to the repository. Inspect generated-file headers, build.yaml, l10n.yaml, and scripts instead of assuming every .g.dart follows the same process.
The Dart documentation describes build_runner as the command layer used by builders such as json_serializable (build_runner). Flutter’s localization guide identifies ARB files as inputs and AppLocalizations as generated output (internationalization).
A safe change has four stages:
- edit the source model, annotation, or ARB resource;
- inspect the source diff;
- run the repository’s existing generation command;
- inspect generated changes separately.
Do not universally ban the agent from running generation. Let it run a known command after the source diff is approved, with a clean worktree and a clear list of expected outputs. Stop if the generator rewrites unrelated files.
Run the existing code-generation command.
Do not alter dependencies or generator configuration.
After it finishes, report every changed generated file.
Stop if any file outside the expected list changes.
4. Verify async lifecycles, not just syntax
A frequent Flutter defect appears after await:
onPressed: () async {
await controller.save();
if (!context.mounted) return;
context.pop();
}
Flutter’s BuildContext documentation says not to cache a context and to check mounted when using it across an asynchronous gap (BuildContext).
That check is necessary, but it is not the entire review. Ask:
- Can the user tap twice before the first operation completes?
- Is cancellation possible?
- Does the callback use the intended router or navigator scope?
- What happens on failure?
- Can late state overwrite a newer request?
- Is loading state cleared in
finally?
Riverpod has its own lifecycle semantics, and they vary by version. Riverpod 3 introduced Ref.mounted, automatic retry, and lifecycle changes; its documentation explicitly recommends a careful migration (Riverpod 3 changes, migration guide). Use those APIs only if the locked package version supports them.
Do not ask an agent to “fix stale context everywhere.” Give it one flow, one reproduction, and the exact expected behavior.
5. Specify UI constraints that can be tested
A screenshot-sized prompt invites a screenshot-sized implementation. Flutter layout depends on constraints, text, insets, and the local position in the widget tree.
For a new component, specify states and boundaries:
States:
loading, loaded, empty, error, offline
Content:
long title, missing image, localized price, optional subtitle
Constraints:
works in the parent width supplied by LayoutBuilder
supports text scaling
handles keyboard and safe-area insets
keeps primary action reachable
uses existing breakpoints and design tokens
Interactions:
tap, back, retry, double tap, loading lock
Flutter’s adaptive guidance distinguishes window size from local widget constraints: use MediaQuery.sizeOf for the app window and LayoutBuilder when the component should respond to constraints from its parent (adaptive layout guidance). “Test on a phone” is not enough for split-screen, tablet, foldable, desktop, or large text.
Ask the agent for a widget test that covers the component’s states. Then inspect the running screen at the supported widths and platforms. Golden tests can catch visual drift, but they do not prove navigation, keyboard, accessibility, or native plugin behavior.
6. Match the test to the changed boundary
Flutter documents three broad levels: unit, widget, and integration tests (testing overview).
Use them deliberately:
- Unit test: pure mapping, validation, policy, and controller logic.
- Widget test: rendering and interactions inside the Flutter widget environment.
- Integration test: a complete flow on a target device or emulator.
- Platform build or device check: Gradle, Xcode, permissions, deep links, notifications, camera, WebView, and other native boundaries.
An agent often over-mocks because that makes a test easy to write. Require the test to pass through the production code path under review and fake only the external boundary. A test of a copied implementation proves little.
For native user flows, keep a small E2E suite. Our Maestro guide for Flutter covers permissions and cross-platform flows, while the official integration_test package covers app-level Flutter tests on devices and emulators (integration tests).
7. Use the official MCP server as a tool bridge
Dart 3.9 and later include the experimental Dart and Flutter MCP server. It can expose analyzer diagnostics, symbol information, tests, formatting, pub.dev search, runtime errors, widget-tree inspection, and interaction with a running app (official MCP server).
For supported clients, the basic command is:
dart mcp-server
The server helps replace “this code looks right” with direct feedback. It does not make every available action safe. Package management still changes project configuration; runtime interaction can require a debug extension; roots and client permissions determine what the agent can see.
Apply the same least-privilege rules used for production MCP servers. Start with analysis, tests, and read-only runtime inspection. Require approval for dependencies, config changes, code generation that touches many files, and any command outside the repository.
Flutter also publishes adaptable AI rule templates in several sizes (official AI rules). Use them as a baseline, then add only rules that are specific and enforceable in your repository:
Source of truth for localization: lib/l10n/*.arb
Generated outputs: lib/generated/**; never edit directly
State pattern: copy the nearest feature; no new framework
Navigation: use named routes from app_router.dart
Dependencies and native config: require approval
Required checks: dart format, flutter analyze, targeted tests
A rules file should point to source files and commands. It should not become a second, stale architecture manual.
8. Close every task with evidence
A good completion report is short:
Changed:
- saved_places_controller.dart: added guarded remove flow
- place_details_page.dart: wired confirmation and existing error state
- two tests: success and repository failure
Ran:
- dart format <changed Dart files>
- flutter analyze
- flutter test test/features/saved_places/
Not run:
- iOS build; no native files changed
- full integration suite; unrelated to this isolated flow
Risks:
- offline retry behavior is unchanged and remains outside scope
The default verification ladder is:
dart format <changed-dart-files>
flutter analyze
flutter test <targeted-path>
flutter test
Add generation, golden updates, integration tests, or platform builds only when the boundary requires them. Always inspect git diff after commands that can rewrite files.
Does AI make Flutter development faster?
There is no honest universal percentage.
METR’s randomized study found experienced developers working on familiar open-source repositories took longer with the early-2025 AI tools in that experiment (study). It did not measure Flutter teams as a category, and it does not prove that every coding agent slows every developer.
Measure your workflow instead:
- accepted diffs without manual rewrite;
- defects found before review;
- review and rework time;
- escaped regressions;
- time by task class: models, state, UI, native configuration, tests.
In my Flutter work, agents are most useful when the contract is explicit and the repository already contains a pattern to copy. They are least trustworthy when visual behavior, asynchronous ownership, and native platform configuration are left implicit.
The practical rule is simple: use the agent to shorten the edit-feedback loop, never to remove the feedback.