Skip to content

Flutter 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.md block 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 (rg guards, build_runner --only-check, table-driven tests) that replace reading the generated code
  • A failure map for build_runner drift, 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.

PackageVersionWhat changed that an agent gets wrong
flutter_riverpod3.4.3 (needs Dart ^3.12)StateNotifierProvider, StateProvider, and ChangeNotifierProvider live in package:flutter_riverpod/legacy.dart. Failing providers retry automatically.
riverpod_annotation / riverpod_generator4.0.7 / 4.0.9The 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_lint3.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_router18.0.118.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.
dio5.11.1Stable 5.x API: InterceptorsWrapper, DioException.
freezed4.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_runner2.16.1Fixes 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.

Terminal window
# Marketplace and plugin are both named dart-flutter
claude plugin marketplace add flutter/agent-plugins
claude plugin install dart-flutter@dart-flutter

To add only the MCP server, run claude mcp add dart -- dart mcp-server. Open /mcp in a session to confirm the server connected.

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.

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 test

The 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.

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:

Terminal window
# Must print nothing: legacy providers or pre-3.0 generated refs
rg -n "flutter_riverpod/legacy.dart|StateNotifier|ProviderRef\b|FutureProviderRef" lib
# Generated files must match what the generator writes
dart run build_runner build --only-check

The --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:

Terminal window
rc=0
rg -n 'package:(flutter|dio)/' lib/features --glob '**/domain/**' || rc=$?
test "$rc" -eq 1

Verify 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.

  1. Generated code is current. dart run build_runner build --only-check (if you commit generated files), otherwise dart run build_runner build.
  2. Format and static analysis. dart format --output=none --set-exit-if-changed . and flutter analyze. Treat riverpod_lint findings as errors.
  3. Guards. The rg checks for legacy Riverpod imports and domain-layer imports from the sections above.
  4. Unit and widget tests. flutter test, including the redirect table and the Dio tests.
  5. Golden tests on one OS. Golden tests run with flutter test on the CI Linux image and compare against the committed images. Regenerate them with flutter test --update-goldens only on that image, in a separate commit.
  6. Integration tests on a device. flutter test integration_test on 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_runner drift. Symptoms: a provider that “does not exist”, a stale .g.dart, or a type error in generated code. Recovery: run dart run build_runner clean, then dart run build_runner build. The --delete-conflicting-outputs flag no longer does anything in build_runner 2.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 missing part directive, a legacy and codegen syntax mix, or riverpod_annotation and riverpod_generator on different majors.
  • Legacy Riverpod creeps back. Symptom: the rg guard finds legacy.dart or StateNotifier. Recovery: reject the change and paste one of your existing providers into the prompt as a style anchor. Add the rule to AGENTS.md if 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 retry function to ProviderScope that returns null for ApiFailure, as in the Dio prompt.
  • Tests expect the raw error. Symptom: a test that expects ApiFailure from ref.read or ref.watch fails after the Riverpod 3 upgrade. Cause: Riverpod 3 wraps a rethrown provider error in ProviderException. Recovery: assert on the provider’s AsyncValue state, or unwrap the exception, and tell the agent which one your suite uses.
  • freezed 4 resolves as 3.2.x. Symptom: flutter pub add freezed picks 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” in AGENTS.md so the agent targets the version it gets.
  • go_router 18 will not resolve. Symptom: flutter pub get fails on the SDK constraint. Cause: 18.0.0 needs Flutter 3.44 and Dart 3.12. Recovery: pin go_router: ^17.5.0 until 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 update or 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_errors return nothing. Cause: the app runs in release mode or with --no-dds, so no Dart Tooling Daemon is registered. Recovery: run flutter run in debug mode and ask the agent to connect to it through the dtd tool.

Did this guide help you?

One tap, no sign-in.

not at all a lot