Flutter Development Patterns
PLFlutter development with Claude Code, Codex, or Cursor produces verifiable code when the agent starts from three pinned facts: the package versions (Riverpod 3 with code generation, go_router, Dio, freezed), the architecture rules, and one check command. The Flutter team’s agent plugin and the Dart MCP server then verify each change through analysis, code generation, and tests.
This page is for developers and tech leads who ship a Flutter app with an agent. You ask for a provider and get a StateNotifierProvider, which Riverpod 3 moved to a legacy.dart import. You ask for a Dio retry interceptor, and a failing screen now fires dozens of requests, because Riverpod 3 also retries failing providers on its own. Neither bug shows up in a diff you skim.
What you’ll walk away with from Flutter development patterns
Section titled “What you’ll walk away with from Flutter development patterns”- The Flutter team’s agent plugin (skills plus the Dart MCP server), installed per tool
- An
AGENTS.mdblock that pins the stack and the check command, so every session inherits them - Copy-paste prompts for a code-generation Riverpod provider, a go_router auth redirect that cannot loop, a Dio service with typed failures, and widget tests
- Mechanical checks for each recipe (
rgguards,build_runner --only-check, table-driven tests) that replace reading the generated code - A failure map for
build_runnerdrift, double retries, go_router’s SDK floor, Gradle and CocoaPods breaks, and golden-test flakiness
Which Flutter package versions does this page assume?
Section titled “Which Flutter package versions does this page assume?”The agent’s training data spans several major versions of every package below, and each major changed an API the agent likes to use. State the versions in the prompt and in AGENTS.md. The table lists what pub.dev served on 2026-09-26.
| Package | Version | What changed that an agent gets wrong |
|---|---|---|
flutter_riverpod | 3.4.3 (needs Dart ^3.12) | StateNotifierProvider, StateProvider, and ChangeNotifierProvider live in package:flutter_riverpod/legacy.dart. Failing providers retry automatically. |
riverpod_annotation / riverpod_generator | 4.0.7 / 4.0.9 | The generator’s major (4) differs from the runtime’s (3). Keep annotation and generator on the same major. Providers take a plain Ref; the generated UserProfileRef-style subclasses are gone. |
riverpod_lint | 3.1.9 (needs Dart 3.13) | Built on analysis_server_plugin, not custom_lint. Follow its README, not a custom_lint recipe from an older article. |
go_router | 18.0.1 | 18.0.0 requires Flutter 3.44 / Dart 3.12. On an older Flutter, pin ^17.5.0. URLs are case-sensitive since 15.0.0. |
dio | 5.11.1 | Stable 5.x API: InterceptorsWrapper, DioException. |
freezed | 4.0.2 (needs Dart 3.13) | 4.0.0 no longer accepts final inside constructor parameters. Classes need sealed or abstract (since 3.0). |
build_runner | 2.16.1 | Fixes incorrect generated files by default. --delete-conflicting-outputs is a removed flag that is ignored with a warning. --only-check fails the build when files on disk differ from what the generator would write. |
Let the agent add packages with flutter pub add <package>, which resolves a version compatible with your SDK, rather than typing version numbers into pubspec.yaml.
Install the Flutter team’s agent plugin and the Dart MCP server
Section titled “Install the Flutter team’s agent plugin and the Dart MCP server”The Flutter team publishes flutter/agent-plugins, renamed from flutter/skills. Its dart-flutter plugin (1.0.6 on 2026-09-26) bundles 25 skills, 10 for Flutter and 15 for Dart, such as flutter-add-widget-test, flutter-fix-layout-issues, and dart-resolve-package-conflicts. It also registers the Dart MCP server (dart mcp-server, which ships with the Dart SDK). The Dart team also publishes its skills in dart-lang/skills.
# Marketplace and plugin are both named dart-flutterclaude plugin marketplace add flutter/agent-pluginsclaude plugin install dart-flutter@dart-flutterTo add only the MCP server, run claude mcp add dart -- dart mcp-server. Open /mcp in a session to confirm the server connected.
# The repository ships a Codex marketplace named dart-fluttercodex plugin marketplace add flutter/agent-pluginscodex plugin add dart-flutter@dart-flutterTo add only the MCP server, run codex mcp add dart -- dart mcp-server.
The repository also ships a Cursor plugin manifest (.cursor-plugin/plugin.json); the skills CLI route below works without it.
Install the skills with the cross-agent skills CLI (skills 1.7.0 on npm), which writes them to .agents/skills:
npx skills@latest add flutter/agent-plugins --skill '*' --agent universal --yesThen add the Dart MCP server to .cursor/mcp.json:
{ "mcpServers": { "dart": { "command": "dart", "args": ["mcp-server"] } }}The MCP server is what changes the loop. Without it, the agent writes a widget and hopes. With it, the agent can call analyze_files, read get_runtime_errors from the running app, trigger hot_reload, and walk the tree with widget_inspector (tool names from the dart-lang/ai README, read 2026-09-26). The server needs Dart 3.9 or later. It reaches a running app through the Dart Tooling Daemon, which flutter run starts in debug and profile mode.
For how skills and MCP servers compare, see how to install and manage skills and official vendor skills.
Pin the Flutter stack in AGENTS.md
Section titled “Pin the Flutter stack in AGENTS.md”A prompt pins the stack for one session. A project instruction file pins it for every session. Codex reads AGENTS.md, and Claude Code reads it when the repository has no CLAUDE.md (from v2.1.277, both release channels on 2026-10-12). If you keep a CLAUDE.md, put the same block there. In Cursor, put the same block in a project rule (Cursor Rules). Copy this block and adjust the versions.
## Flutter stack (do not substitute)- Flutter 3.47+, Dart 3.13+. Add packages with `flutter pub add`, never by typing versions.- State: Riverpod 3 with code generation (`@riverpod`, `part '*.g.dart';`). Never import `package:flutter_riverpod/legacy.dart`. No StateNotifier, no get_it.- Navigation: go_router 18 with one `GoRouter` provided through Riverpod.- Networking: Dio behind `ApiService`. Map every `DioException` to the sealed `ApiFailure`.- Models: freezed 4 (`sealed` or `abstract` classes). No hand-written fromJson.- Layout: lib/features/<feature>/{data,domain,presentation}. domain/ imports no Flutter and no Dio.
## Check command (run before you say a task is done)dart run build_runner build && dart format --output=none --set-exit-if-changed . \ && flutter analyze && flutter testThe last line is the contract. An agent that reports “done” without a green check command has not finished, and you can hold it to that without opening a single file.
Run the prompt in whichever tool you use. In Claude Code, pass it as the first argument (claude "…"). In Codex, pass it the same way (codex "…"); the session asks before commands that need approval. In Cursor, paste it into the agent chat. Each tool follows the block only from the file it reads. When you change the stack, update every copy of the block in the same commit.
Write a code-generation Riverpod provider
Section titled “Write a code-generation Riverpod provider”The payoff of @riverpod is that loading, caching, and parameters become declarative. The risk is that the agent mixes in legacy provider classes from its training data.
Check the result mechanically instead of reading it:
# Must print nothing: legacy providers or pre-3.0 generated refsrg -n "flutter_riverpod/legacy.dart|StateNotifier|ProviderRef\b|FutureProviderRef" lib# Generated files must match what the generator writesdart run build_runner build --only-checkThe --only-check line matters only if you commit the .g.dart and .freezed.dart files. If you commit them, it catches the agent that edited a generated file by hand or forgot to rerun the generator.
Build a go_router auth redirect that cannot loop
Section titled “Build a go_router auth redirect that cannot loop”The classic go_router bug is an infinite redirect: the guard sends signed-out users to /login but forgets to let /login through. The fix is one guard clause, and it is the line the agent most often omits. Do not trust your eyes to find it. Make the redirect a pure function and test it.
The table-driven test is the acceptance criterion. If it lists every status and route pair and passes, the redirect cannot loop, whatever the implementation looks like. The /LOGIN row matters because go_router URLs have been case-sensitive since 15.0.0.
Build a Dio API service with typed failures and one retry layer
Section titled “Build a Dio API service with typed failures and one retry layer”The happy path of a Dio service is trivial. The production parts are the 401 refresh-and-replay, the typed failure, and where retries happen.
Riverpod 3 retries a failing provider by default. In riverpod 3.4.3, ProviderContainer.defaultRetry allows up to 10 retries, from 200 ms doubling to a 6.4 s cap, for any failure that is not an Error or a ProviderException. An ApiFailure is neither. Add a Dio interceptor that retries three times, and one failing screen can send dozens of requests. Pick one layer for retries.
The three tests prove the behaviour that matters. Pair them with one mechanical guard: rg -n "rethrow|throw e\b" lib/core/network should print nothing once every DioException is mapped. For the API contract on the server side, see API development patterns.
Keep clean-architecture layering honest with a check
Section titled “Keep clean-architecture layering honest with a check”For a larger app, ask for a concrete feature, not “set up clean architecture”. The constraint that matters is that domain imports neither Flutter nor Dio. A leaked package:flutter/material.dart in a use case means the layering is cosmetic, and a grep finds it faster than a reviewer.
Add the layering rule to CI so it holds after the agent’s next hundred edits. The step passes only when rg reports no match (exit 1); a match (exit 0) or an rg error such as a missing directory (exit 2) fails it, and the || rc=$? form keeps working under the bash -e shell that CI runners use:
rc=0rg -n 'package:(flutter|dio)/' lib/features --glob '**/domain/**' || rc=$?test "$rc" -eq 1Verify each Flutter change without reading every line
Section titled “Verify each Flutter change without reading every line”The check command in AGENTS.md is the inner loop. The pull request needs a gate that runs the same checks plus the ones too slow for every edit. The order below is cheapest first, so a failure costs as little CI time as possible.
- Generated code is current.
dart run build_runner build --only-check(if you commit generated files), otherwisedart run build_runner build. - Format and static analysis.
dart format --output=none --set-exit-if-changed .andflutter analyze. Treatriverpod_lintfindings as errors. - Guards. The
rgchecks for legacy Riverpod imports and domain-layer imports from the sections above. - Unit and widget tests.
flutter test, including the redirect table and the Dio tests. - Golden tests on one OS. Golden tests run with
flutter teston the CI Linux image and compare against the committed images. Regenerate them withflutter test --update-goldensonly on that image, in a separate commit. - Integration tests on a device.
flutter test integration_teston an emulator for the flows that touch plugins, such as sign-in and payment. The mobile app testing patterns page covers device farms and flaky-test triage.
Who signs off: the pull-request reviewer confirms the gate is green and that the tests match the acceptance criteria in the task, such as the redirect table and the three Dio cases. Human reading goes to those tests and to the domain layer, not to generated code or widget boilerplate. For the review process itself, see how to review agent pull requests.
To have the agent write the CI job itself, connect the GitHub MCP server first, so it reads your real workflows and secret names instead of inventing them. See the GitHub MCP server guide for per-tool setup and CI/CD pipeline patterns for the job structure. For production crashes, an observability MCP server such as Sentry’s lets the agent read the real stack trace for a release before it proposes a fix.
What breaks when agents write Flutter code?
Section titled “What breaks when agents write Flutter code?”build_runnerdrift. Symptoms: a provider that “does not exist”, a stale.g.dart, or a type error in generated code. Recovery: rundart run build_runner clean, thendart run build_runner build. The--delete-conflicting-outputsflag no longer does anything inbuild_runner2.16.1, so an agent that keeps adding it has not found the cause. Paste the first error into the agent and ask which of three causes it is: a missingpartdirective, a legacy and codegen syntax mix, orriverpod_annotationandriverpod_generatoron different majors.- Legacy Riverpod creeps back. Symptom: the
rgguard findslegacy.dartorStateNotifier. Recovery: reject the change and paste one of your existing providers into the prompt as a style anchor. Add the rule toAGENTS.mdif it is missing. - A failing screen floods the API. Symptom: logs show the same GET many times per screen open. Cause: Riverpod’s automatic retry on top of a Dio retry interceptor. Recovery: pass a
retryfunction toProviderScopethat returnsnullforApiFailure, as in the Dio prompt. - Tests expect the raw error. Symptom: a test that expects
ApiFailurefromref.readorref.watchfails after the Riverpod 3 upgrade. Cause: Riverpod 3 wraps a rethrown provider error inProviderException. Recovery: assert on the provider’sAsyncValuestate, or unwrap the exception, and tell the agent which one your suite uses. - freezed 4 resolves as 3.2.x. Symptom:
flutter pub add freezedpicks freezed 3.2.5 and riverpod_lint 3.1.8, and the agent’s freezed 4 code fails to build. Cause: freezed 4.0.0 and later and riverpod_lint 3.1.9 need Dart 3.13, which ships with Flutter 3.47. Recovery: upgrade to Flutter 3.47+ in its own change, or, on Dart 3.12, write “freezed 3” inAGENTS.mdso the agent targets the version it gets. - go_router 18 will not resolve. Symptom:
flutter pub getfails on the SDK constraint. Cause: 18.0.0 needs Flutter 3.44 and Dart 3.12. Recovery: pingo_router: ^17.5.0until you upgrade Flutter. Do not let the agent upgrade the Flutter SDK as a side effect of a feature task. - Gradle and CocoaPods failures. Android failures after a dependency bump are usually an Android Gradle Plugin, Gradle, or Kotlin version mismatch. iOS failures are usually a pod that needs
pod repo updateor a higher deployment target. Recovery: paste the full build log, ask the agent to pin the specific plugin versions, and forbid it from changing the Flutter SDK version to fix a pod. - Goldens pass locally and fail in CI. Cause: macOS and Linux render fonts differently. Recovery: generate goldens only on the CI image and load a bundled font in
flutter_test_config.dart. - The agent cannot see the running app. Symptom: Dart MCP tools such as
get_runtime_errorsreturn nothing. Cause: the app runs in release mode or with--no-dds, so no Dart Tooling Daemon is registered. Recovery: runflutter runin debug mode and ask the agent to connect to it through thedtdtool.
Where to go next with Flutter
Section titled “Where to go next with Flutter”- Mobile development overview for how the Flutter, Expo, and React Native pages divide the work
- React Native and Flutter UI recipes for widget-level recipes such as custom widget libraries and platform-adaptive design
- Expo development patterns and React Native development patterns if you are comparing cross-platform stacks
- Native iOS and Android recipes for the platform code behind a Flutter plugin
- Test-driven development with agents for writing the table-driven test before the agent writes the redirect
Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in