How to Review AI-Generated Flutter Code (Before It Breaks Production) A developer found that AI-generated Flutter code consistently makes seven structural mistakes, including bad state management, missing tests, and hardcoded values. One example involved an agent using a subprocess call to `curl` for a GET request instead of Dart's built-in HTTP clients, highlighting a pattern where agents optimize for statistically dominant patterns over idiomatic code. The developer recommends writing tests first and deriving values from state to avoid these issues. Every unsupervised AI agent we've reviewed that wrote Flutter code made the same seven mistakes. These aren't typos or stylistic differences. They're structural failures that compound—bad state management plus missing tests plus hardcoded colors means the codebase becomes expensive to theme, hard to test, and impossible to maintain at scale. Here's a small one to set the tone: a developer asked an agent to implement a GET request to an external service in a Dart project. The agent's solution was to shell out to curl via Process.run and parse the stdout. Not package:http . Not dio . Not even dart:io 's own HttpClient . A subprocess call to a CLI tool, inside a language that's had first-class HTTP clients since Dart 1.0. That one is worth sitting with, because it's not really a Flutter problem — it's the whole pattern in miniature. The agent wasn't "wrong" that curl can make a GET request. It optimized for "this pattern appears constantly in training data" over "this is the idiomatic way to do it in the language I'm currently writing." Bash and curl show up in approximately every tutorial, README, and Stack Overflow answer ever written. package:http shows up in Dart-specific docs. Given no other constraint, the agent reached for the statistically dominant pattern, not the contextually correct one. The seven gaps below are the same failure mode, just less obvious than "shells out to curl." Here's what we found, with real code examples and the fixes that work. The Problem: Agents recalculate the same values across multiple locations instead of maintaining one source of truth. Imagine a checkout flow where the cart total is computed three separate ways: items.sum + tax - discount items.sum - discount + tax items.sum - discount 1 + taxRate Different calculations. Same semantic meaning. One will break first. The Fix: Derive values once in the state layer using streams. Let all widgets read from that single source: // Instead of computing at call sites: final total = items.fold 0, sum, item = sum + item.price ; // Compute once, derive everywhere: final totalStream = itemsStream.map items { final subtotal = items.fold 0, sum, item = sum + item.price ; final tax = subtotal taxRate; final withDiscount = subtotal - appliedDiscount; return withDiscount + tax; } ; The principle: if it can be computed from state, it is not state. Every recomputation is a sync bug waiting to happen. The Problem: "It ships the feature and stops. No widget tests, no goldens, no benchmark." Agents don't write tests because they can't run them in isolation. They generate code, you integrate it, and only then do you see: Tests become feedback loops the agent can iterate against. Golden files provide visual regression detection—catching overflow, dark-mode contrast failures, and text truncation at 200% scale automatically. The Fix: Write tests first , then tests become the specification: group 'SearchPage', { testWidgets 'shows spinner while loading', tester async { await tester.pumpWidget SearchPage state: const SearchState.loading ; expect find.byType CircularProgressIndicator , findsOneWidget ; } ; testWidgets 'shows error and allows retry', tester async { final state = SearchState.failed error: AppError.network ; await tester.pumpWidget SearchPage state: state ; expect find.text 'Network error' , findsOneWidget ; await tester.tap find.byIcon Icons.refresh ; expect find.byType CircularProgressIndicator , findsOneWidget ; } ; testWidgets 'respects 200% text scale', tester async { tester.binding.window.textScaleFactorTestValue = 2.0; addTearDown tester.binding.window.clearTextScaleFactorTestValue ; await tester.pumpWidget SearchPage state: const SearchState.idle ; // No overflows expect find.byType OverflowBox , findsNothing ; } ; } ; The Problem: Agents use loose parallel boolean fields and branch over them, creating unmaintainable conditionals. bool isLoading = false; String? error; List