跪拜 Guibai
← Back to the summary

Sonnet 5's Flutter Code Keeps Going Rogue — Here's the Constraint System That Stops It

AI-Driven Flutter Engineering: How to Keep AI-Generated Code on Track

Over the past six months, models like Claude Sonnet 5, GPT-4.1, and Gemini 2.5 Pro have seen their scores on SWE-bench and Terminal-Bench skyrocket. Tools like Cursor and Claude Code have also turned "AI writing Flutter code" from a toy into a productive tool. But almost every heavy user runs into the same problem: AI-generated code is getting stronger, but it's also getting easier to "go off track" — you ask it to change a ListTile, and it casually refactors the entire LoginPage; you ask it to fix a bug, and it takes the liberty of swapping out your state management solution.

This isn't the model getting dumber; it's that constraints haven't kept pace with its growing capabilities. This article, based on real-world scenarios using Cursor + Sonnet 5 + Flutter, dissects the root causes of going off track and provides a proven system to prevent it.


1. Four Root Causes of Going Off Track

Type Typical Manifestation Root Cause
Scope Drift While changing file A, it casually changes B/C/D, even refactoring unrelated modules Fuzzy instruction boundaries + Sonnet 5's agentic tendency (Microsoft testing shows 5 is more prone to "incidental optimization" than 4.6)
Paradigm Wobble Switches between Riverpod and Bloc; mixes find.text and find.byKey Project conventions are not anchored in a place visible to the AI, or the rules file is too long for the model to read completely
Context Poisoning After round 10, the model starts forgetting "do not rewrite the entire build" and pastes the whole thing again Long sessions not compacted/restarted in time; early constraints are diluted by subsequent dialogue
Self-Check Hallucination Model claims "ran tests and all passed," but it didn't actually run them or ran the wrong files Sonnet 5's agentic self-check + noisy terminal output, leading it to mistakenly believe execution was successful

These four root causes are especially prominent in Flutter projects—deep widget nesting, long build methods, multiple state management options, and generated files (.g.dart) mixed with handwritten files. A moment's inattention and the model oversteps.


2. The Anti-Drift System: From Hard Constraints to Soft Habits

1. Use AGENTS.md + .cursor/rules/*.mdc for a Two-Layer Paradigm Anchor

Many developers cram dozens of rules into a single .cursorrules file. The result: with alwaysApply too long, Sonnet 5's new tokenizer eats 30% of the capacity, and the model never finishes reading it. The correct approach is layered anchoring:

Layer 1: AGENTS.md (project root, no more than 50 lines)

This file is the universal entry point for all AI tools (Cursor, Claude Code, Codex, Windsurf). Just place it in the project root. Put only non-negotiable red lines here:

# Flutter Project Paradigm (Non-negotiable)

- State Management: Riverpod (AsyncNotifier + freezed sealed)
- Prohibited: Mixing Bloc / Provider / setState
- Widget Finding: Must use WidgetKeys constants (`find.byKey(WidgetKeys.xxx)`), `find.text()` is prohibited
- Code Generation: freezed / json_serializable / riverpod_generator, only change annotations, editing *.g.dart is prohibited
- UI changes must @ file+line number, vague instructions like "change the login page" are prohibited
- Prohibited from rewriting the entire build method, only change the specified sub-tree ±5 lines

Layer 2: .cursor/rules/*.mdc (multi-file, loaded on-demand via globs)

This is the multi-file rule system recommended by Cursor 0.45+. Use globs to match different file types; only rules matching the current file are injected:

.cursor/rules/
├── 000-core.mdc          # alwaysApply: true, ≤500 chars, general token saving
├── 010-effort.mdc        # alwaysApply: true, controls Sonnet 5 thinking
├── 100-flutter-style.mdc # globs: **/*.dart, Flutter coding conventions
├── 101-state.mdc         # globs: lib/**, state management paradigm
├── 102-test.mdc          # globs: **/*_test.dart, testing paradigm
└── 103-assets.mdc        # globs: pubspec.yaml, protects pubspec

Key Point: The total word count for alwaysApply must be kept under 500 characters (Sonnet 5's new tokenizer can make the same text consume 30% more tokens). Anything beyond that should be split into dedicated files using globs. This way, the model reads the core red lines every round without being overwhelmed by a sea of rules.


2. Shift Instructions from "Open Intent" to "Constraint-Based Prompts"

The primary trigger for going off track is overly open instructions. If you just say "help me optimize the login page," the model will naturally "optimize" according to its own understanding—including refactoring routes, adjusting themes, and rewriting state management. The correct approach is to draw boundaries within the instruction:

❌ High Drift Risk ✅ Constraint-Based
"Help me change the login page button to rounded corners" "@lib/pages/login_page.dart L78, change the ElevatedButton's shape to RoundedRectangleBorder(borderRadius: 12), only change this one widget, do not change the rest of build, do not change AuthBloc"
"Add error handling to network requests" "In the login notifier's signInWithPassword, catch DioException, emit LoginState.error(message), do not add new providers, do not change UI"
"Run the tests" "Run flutter test test/features/login/, only fix failing cases, do not add new tests, do not touch test/shared/ "

Three elements are indispensable: Where to change (@file+L#) + What to change + What NOT to change. The "what not to change" is often more important than the "what to change"—Sonnet 5's agentic nature dictates that if you don't draw a boundary, it will cross it.


3. For Complex Tasks, Plan First, Then Execute

For tasks like cross-file refactoring, new features, or Router structure adjustments, don't let the model write code directly. In Cursor Composer or Claude Code, have it do this first:

"First, produce a change plan: list the files to be modified, the scope of changes for each file, and the verification commands involved. I will confirm before execution."

During the Plan phase, you only need to watch three things:

A round of planning usually costs only 200-300 tokens but can save 3-5 rounds of rework later. Planning isn't wasting tokens; it's buying insurance.


4. Session Management: One Task Per Session, Compact/Restart Promptly

The high-risk period for going off track is rounds 10-20—early constraints are diluted by subsequent dialogue, and the model starts "freestyling."


5. Two Flutter-Specific Anti-Drift Hooks

WidgetKeys as Constants

The most common drift in Flutter testing and UI is the model oscillating between find.text('Login') and find.byKey(WidgetKeys.loginSubmit). The solution is to maintain key constants uniformly under lib/core/keys/:

abstract class LoginKeys {
  static const emailField = Key('login_email_field');
  static const passwordField = Key('login_password_field');
  static const submit = Key('login_submit');
}

Then, hardcode in 100-flutter-style.mdc: "New interactive elements must add WidgetKeys; changes to existing finds must use WidgetKeys.xxx." Once the model sees a constant reference, it no longer dares to arbitrarily change it to find.text.

The build_runner Moat

In Flutter projects, the model hand-writing .g.dart files is a disaster—it thinks it's "saving you a step," but the result is hand-written generated code that doesn't match the annotations, causing compilation failures. Hardcode in AGENTS.md:

"When changing a model, only change annotations. After changing, prompt the developer to run flutter pub run build_runner build --delete-conflicting-outputs. Hand-writing *.g.dart is prohibited."

Also, exclude *.g.dart, *.freezed.dart, *.config.dart in .cursorignore so the model can't even read these files, fundamentally preventing hand-writing.


3. Early Signals of Drift: Nip It in the First Round

Don't wait until round 5 to realize the direction is wrong. If any of the following appear in the model's first-round reply, interrupt immediately and re-issue the instruction:


4. Summary

In AI-driven Flutter engineering, the core contradiction is not "can the model write it," but "can the model write ONLY what you told it to write." Preventing drift doesn't rely on "smarter models," but on a set of enforceable constraint systems:

Level Tool Function
Red Line Anchor AGENTS.md (<50 lines) Cross-tool universal, non-negotiable
Layered Details .cursor/rules/*.mdc (globs on-demand loading) Injected by file type, alwaysApply ≤500 chars
Instruction Design Constraint-based prompt (Where + What + What NOT) Draw boundaries for the model
Task Workflow Plan → Confirm → Execute Align on complex tasks first
Session Hygiene One task per session + prompt compact/restart Prevent constraint dilution
Flutter Specialization WidgetKeys constants + build_runner moat Plug the two highest-frequency drift points

This system has been validated across multiple Flutter production projects: with the same Cursor Pro $20 quota, it used to run out in half a month; after configuration, it lasts a full month, and code quality is noticeably improved—not because the model got stronger, but because it finally knows what it shouldn't do.

The future of AI coding is not "an all-powerful model," but "a model that is all-powerful within the circle you draw." Draw the circle well, and it won't go off track.