Kort samengevat. Dit is geen pleidooi tegen AI die Flutter schrijft. Agents schrijven een groot deel van onze code, en we richtten ons proces daar bewust op in. Het is een pleidooi over wat er gebeurt wanneer niemand ze aanstuurt. Wij leveren Flutter op met agents, en we beoordelen Flutter die agents onbewaakt schreven — en dezelfde zeven gaten scheiden de twee. Een onbewaakte agent berekent afgeleide state overal opnieuw in plaats van één bron van waarheid te houden; schrijft geen tests en geen benchmarks, waardoor de beoordeling de enige poort is en regressies vrij doorstromen; klapt complexe asynchrone logica in tot if- en switch-piramides over losse vlaggen in plaats van haar als stroom te modelleren; behandelt designtokens als constanten in een constants.dart in plaats van als de ThemeData die het framework hem al geeft; verzint op elk scherm een verse UX-woordenschat, omdat hij de app niet kan zien; plakt tekenreeksen aan elkaar waar meervoudsregels uit ICU verplicht zijn; en vereenvoudigt uniform, waarmee hij juist de delen van het domein platslaat waar complexiteit het punt is. Niets hiervan is een Flutter-probleem, en niets ervan betekent dat de code waardeloos is. Het betekent dat de systeembrede besluiten nog steeds van ons zijn.


Waarom Flutter dit harder afstraft dan de meeste stacks

Elk van deze fouten duikt in elke taal op. Flutter maakt ze alleen luider, om vier structurele redenen.

De interface is code, helemaal tot onderin. Er zit geen stylesheet buiten de broncode die de boel samenhangend houdt, geen cascade, geen gedeeld CSS-bestand dat een tweede developer wel moet opmerken. Elke pixel is een Dart-uitdrukking. Wordt consistentie niet binnen de code afgedwongen, dan dwingt niets anders haar af.

Alles compileert. Flutter biedt ruwweg tien redelijke manieren om iets te doen, en alle tien draaien. Er is geen compileerfout voor "je had hier het thema moeten gebruiken", geen waarschuwing voor "dit is de derde keer dat je dit totaal afleidt", geen lint voor "dit scherm laadt anders dan de andere negenendertig". De terugkoppeling die de agent nodig heeft bestaat niet tenzij iemand haar bouwt.

De agent ziet het frame nooit. Hij schrijft een scherm, krijgt een schone flutter analyze, en meldt succes — op een lay-out die op 320pt overloopt, de verstuurknop achter het toetsenbord verstopt, en in donkere modus zakt voor contrast. Een mens ziet alle drie in twee seconden kijken. Een agent heeft geen ogen tenzij je hem er een geeft.

De trainingsdata is een decennium gemengde Flutter. Het framework beweegt snel en het corpus niet. Dus schrijven agents zelfverzekerd RaisedButton (verwijderd in 3.0), WillPopScope (opgevolgd door PopScope na 3.12), MediaQuery.of(context).textScaleFactor (vervangen door TextScaler in 3.16), MaterialStateProperty (hernoemd tot WidgetStateProperty in 3.19), Color.withOpacity (uitgefaseerd voor withValues in 3.27), en ThemeData.accentColor, dat in 3.7 werd verwijderd en al jaren niet meer bestaat. Het is niet dat het model fout is — het antwoordt voor een Flutter-versie die niemand meer uitbrengt, en de helft ervan compileert nog steeds met een uitfaseringswaarschuwing die niemand leest.

Dan nu de zeven.


1. Eén bron van waarheid, of overal opnieuw berekenen

Wat de agent doet. Hij leidt dezelfde waarde af op elke plek die hem nodig heeft. Het winkelwagentotaal wordt op de winkelwagenpagina opgeteld. Het wordt nog eens opgeteld in de plakkende afrekenvoet. Het wordt een derde keer opgeteld in de besteloverzicht — en die past de korting vóór belasting toe in plaats van erna. Niets is in één bestand fout; de drie zijn het simpelweg oneens, en dat meningsverschil is onzichtbaar tot een klant het verkeerde bedrag betaalt.

De Flutter-specifieke variant is erger: state in een widget spiegelen.

class _CartPageState extends State<CartPage> {
  double _total = 0; // copy #2 of the truth

  @override
  void initState() {
    super.initState();
    _total = widget.items.fold(0, (sum, i) => sum + i.price * i.qty);
  }
  // ...en nu wordt _total nooit bijgewerkt wanneer het winkelmandje verandert
}

Dit is de klassieke kopie in initState. Het werkt in de demo omdat de demo de winkelwagen nooit wijzigt nadat de pagina is geopend. Het breekt de eerste keer dat een aantal vanuit een bottom sheet wordt gewijzigd.

Wat wij doen. De waarde wordt precies één keer afgeleid, in de statelaag, en elke widget leest hem. Kan hij uit state berekend worden, dan is hij geen state.

// Eén afleiding. De winkelmandjepagina, de voettekst en het overzicht lezen allemaal deze.
Stream<Money> get total => _cart.map(
  (c) => c.items.fold(Money.zero, (sum, i) => sum + i.subtotal),
);

Let op het type Money in plaats van double. Agents grijpen bij geld voortdurend naar double, en 0.1 + 0.2 != 0.3 is niet academisch wanneer het iemands saldo is.

Waarom de agent daar uitkomt. De bestaande afleiding vinden kost context; hem ter plekke opnieuw genereren kost niets. Elke prompt begint vers, dus "is dit al ergens berekend?" is een vraag die hij structureel niet kan stellen. Elke kopie is op zichzelf redelijk. Het afdrijven is wat je betaalt.


2. Tests en benchmarks, of hoop

Wat de agent doet. Hij levert de functie op en stopt. Geen widgettests, geen goldens, geen benchmark. De definitie van klaar is "het compileert en het scherm ziet er plausibel uit in de beschrijving die ik ervan schreef".

Daarmee blijft code review de enige kwaliteitspoort — en dat is het deel dat mensen onderschatten. Beoordelen was al het knelpunt toen mensen de code op menselijke snelheid schreven. Richt drie agents op een codebase en het volume diff dat bij die poort aankomt gaat een orde van grootte omhoog terwijl het aantal mensen dat het kan beoordelen op één blijft. Beoordelen schaalt niet. Uitvoerbare controles wel.

Wat wij doen. We schrijven de tests en de benchmarks eerst, en niet als deugdoefening — als de terugkoppellus die de agent mist.

  • Widgettests voor gedrag: de knop wordt uitgeschakeld tijdens het versturen, de fout verdwijnt bij opnieuw typen, de lijst pagineert op de juiste scrollpositie.
  • voor het uiterlijk. Dit is het waardevolste dat je een agent die aan interface werkt kunt geven, want een golden komt het dichtst bij hem ogen geven. matchesGoldenFile maakt van "brak het herontwerp de lege staat op 320pt in donkere modus bij 200% tekstgrootte?" een controle die in vier seconden in CI draait. Zonder goldens wordt die vraag alleen beantwoord door een mens die de app opent — dat wil zeggen: soms.
  • Benchmarks voor de dingen die stilletjes rotten. Het framebudget is 16,6 ms; een lijst die hapert nadat iemand een schaduw en een Opacity in de itembuilder heeft gezet, is een regressie die geen unittest vangt. Frametijden op een profielbuild, tegen een budget gelegd, vangen het.
  • Een regressietest voor elke bug die we oplossen, zodat de agent die hem oplost hem drie prompts later niet kan terugdraaien.

Het patroon om je eigen te maken: een agent is uitstekend in het bevredigen van een controle die hij kan draaien, en hulpeloos waar geen controle bestaat. Geef hem flutter test en hij itereert tot groen. Geef hem niets en hij vertelt je dat hij er zeker van is. Ons stuk over auditbevindingen beschrijft hoe een codebase eruitziet na een jaar van die tweede optie.


3. Stromen, of een piramide van vlaggen

Wat de agent doet. Hij stelt asynchrone state voor als losse parallelle velden en vertakt daar vervolgens overheen.

bool _isLoading = false;
String? _error;
List<Hit> _items = [];

// build():
if (_isLoading) return const CircularProgressIndicator();
if (_error != null) return Text(_error!);
if (_items.isEmpty) return const Text('Nothing here');
return ListView(/* ... */);

Drie velden, acht mogelijke combinaties, waarvan vier zinloos. Wat wordt er getekend wanneer _isLoading waar is en _error gezet is? Wat de volgorde van de if-regels toevallig oplevert. Dan komt het zoekveld erbij, en voegt de agent een Timer toe voor vertraging, een teller _requestId om verouderde antwoorden weg te gooien, en een mounted-controle vóór elke setState — een met de hand gebouwde, subtiel gebroken herimplementatie van switchMap. Daarnaast groeit een utils.dart met statische hulpfuncties, omdat de logica structureel nergens kan wonen.

Dit is de specifieke vorm: niet dat agents switches vermijden, maar dat ze over vlaggen switchen die ze zelf hebben gezet in plaats van over een type dat de compiler kan controleren.

Wat wij doen. De state is een sealed hiërarchie, zodat onmogelijke combinaties niet weer te geven zijn, en de stroom regelt de timing.

sealed class SearchState {}
final class Idle    extends SearchState {}
final class Loading extends SearchState {}
final class Failed  extends SearchState { Failed(this.error); final AppError error; }
final class Loaded  extends SearchState { Loaded(this.hits); final List<Hit> hits; }

// build() — de compiler dwingt af dat elk geval wordt afgehandeld
return switch (state) {
  Idle()                          => const SearchHint(),
  Loading()                       => const AppSpinner(),
  Failed(:final error)            => AppErrorView(error),
  Loaded(:final hits) when hits.isEmpty => const EmptyResults(),
  Loaded(:final hits)             => HitList(hits),
};

En het asynchrone deel — debounceTime, switchMap en startWith zijn uitbreidingen van rxdart op Stream, niet van dart:async — waar de vlaggenversie stilletjes racet:

// Debounce en annulering zijn declaratief. Een verouderde query kan niet winnen,
// omdat switchMap zijn abonnement al heeft geannuleerd.
Stream<SearchState> get states => _query
    .debounceTime(const Duration(milliseconds: 300))
    .switchMap(_search)
    .startWith(Idle());

Voeg een nieuwe toestand toe — RateLimited, bijvoorbeeld — en de compiler somt elke switch op die bijgewerkt moet worden. In de vlaggenversie betekent een toestand toevoegen elke if-keten met de hand vinden en hopen.

Waarom de agent daar uitkomt. Imperatieve vlaggen zijn het meest voorkomende patroon in het corpus en het makkelijkst regel voor regel te produceren. Een stroom vraagt dat je de hele toestandsmachine tegelijk in je hoofd houdt, en dat is precies waar een generator met beperkte context het slechtst in is. Het is dezelfde fout die we als callbackchaos in audits beschreven — hij draagt alleen een Dart-kostuum.


4. Het framework kennen: ThemeData is geen constantenbestand

Dit is degene waar kennis van het framework het duidelijkst blijkt.

Wat de agent doet. Hij maakt constants.dart, vult dat met static const primary = Color(0xFF6750A4), en schrijft daarna stijlen ter plekke op elke aanroepplek.

Text(
  'Balance',
  style: TextStyle(
    fontSize: 16,
    fontWeight: FontWeight.w600,
    color: AppColors.textPrimary,
  ),
)

Die ene regel heeft vier afzonderlijke problemen, en alleen het eerste is duidelijk:

  1. Donkere modus is nu een handmatige if. AppColors.textPrimary is één kleur. Een tweede thema ondersteunen betekent een ternaire operator op Theme.of(context).brightness op elk van de 200 plekken — en dat is precies wat agents daarna schrijven, scherm voor scherm.
  2. De merkwijziging is een diff van 400 bestanden. Een ontwerper die de typografische ladder wijzigt zou één bestand moeten aanraken. Hier is het zoeken en vervangen door de hele app, en de plekken die zijn afgedreven (fontSize: 15 op twee schermen omdat die generatie anders afrondde) worden gemist.
  3. Tekstschaling van het systeem breekt de lay-out. Hardgecodeerde groottes samengesteld tegen hardgecodeerde marges betekenen dat een gebruiker op 200% toegankelijkheidsschaal overloop krijgt in plaats van herschikking.
  4. Overschrijvingen via Theme.of(context) werken niet meer. Een deelboom in een ander thema verpakken — een donkere sectie op een licht scherm, een gemerkte afrekenpagina in een neutrale app — doet niets, want niets in die deelboom vraagt het thema iets.

Wat wij doen. Het designsysteem leeft in ThemeData, één keer, in de wortel.

MaterialApp(
  theme: AppTheme.light,   // ColorScheme.fromSeed + TextTheme + component themes
  darkTheme: AppTheme.dark,
  // ...
)

// Elke widget vraagt het aan het framework in plaats van aan een constantenbestand:
Text('Balance', style: Theme.of(context).textTheme.titleMedium)

Componentthema's (FilledButtonThemeData, CardThemeData, InputDecorationThemeData) zorgen dat een knop er standaard goed uitziet, zodat de agent die het twintigste scherm produceert het niet fout kan doen, ook al probeert hij het. Voor merktokens waar Material geen plek voor heeft — een grafiekpalet, een verloop, een semantisch "positief/negatief"-paar in een fintech-app — houdt een ThemeExtension ze binnen dezelfde opzoeking in plaats van ernaast:

final class BrandColors extends ThemeExtension<BrandColors> {
  const BrandColors({required this.positive, required this.negative});
  final Color positive;
  final Color negative;
  // copyWith / lerp ...
}

// usage
final brand = Theme.of(context).extension<BrandColors>()!;

Donkere modus kost dan één extra ThemeData, geen ternaire operator per widget. Bij Arcana is het volledige eigen designsysteem zo uitgedrukt, en daarom is een ontwerpherziening daar een diff die je in één beoordeling kunt lezen.

Waarom de agent daar uitkomt. Een constante is lokaal correct en meteen bevredigend: de kleur verschijnt, het scherm bouwt. Een designsysteem door het framework rijgen betaalt zich pas terug over veel schermen en een tweede thema — en terugbetalen-over-de-hele-app is precies de as die een optimalisator per prompt niet kan zien.


5. Ogen, en een decennium opleveren

Wat de agent doet. Hij bouwt veertig schermen, elk op zichzelf verdedigbaar, die samen aanvoelen als een app die uit veertig tutorials is samengesteld — want in zekere zin was dat ook zo.

Laden is een CircularProgressIndicator op het beginscherm, een shimmerskelet op de feed, en een eigen geanimeerd logo op het profiel. Fouten zijn hier een SnackBar, daar rode tekst in de pagina, en op het derde scherm een modale AlertDialog. Een onomkeerbare bevestiging is op het ene scherm een AlertDialog en op het volgende een bottom sheet. Lege staten hebben drie verschillende illustraties, twee verschillende tonen, en één die alleen het woord "Leeg" toont. Sommige formulieren valideren bij elke toetsaanslag, sommige bij versturen, één bij focusverlies. Primaire knoppen zijn in de ene stroom schermbreed en in de andere op de inhoud gepast.

Elk daarvan is een redelijke keuze. Het probleem is dat een gebruiker een app één keer leert. Ziet dezelfde handeling er op drie plekken anders uit, dan vertrouwt hij niet langer dat hij weet wat er gaat gebeuren — en die kosten duiken op in supporttickets, niet in een lintrapport.

Dan is er de klasse defecten die alleen op een scherm bestaat:

  • RenderFlex overflowed by 27 pixels op elk toestel dat smaller is dan de agent zich voorstelde
  • tikdoelen onder 44/48dp, die prima testen en met een echte duim voortdurend missen
  • het toetsenbord dat het veld bedekt waarin wordt getypt, omdat er niets scrolt
  • tekst die op 200% systeemschaal wordt afgekapt
  • inhoud onder de notch of achter de home-indicator, omdat SafeArea geen deel van de prompt was
  • contrast dat in lichte modus slaagt en in donkere zakt

flutter analyze is schoon voor elk van deze.

Wat wij doen. We bepalen de interactiewoordenschat één keer — hoe laden eruitziet, hoe fouten verschijnen, hoe onomkeerbare handelingen bevestigen, wanneer formulieren valideren, hoe navigatie zich gedraagt, wat een lege staat zegt — en daarna bouwt elk scherm dat. Tien jaar en meer opleveren koopt vooral weten welke patronen gebruikers al in de vingers hebben, omdat duizenden andere apps ze daar hebben gezet. Vertrouwd verslaat slim vrijwel altijd, en de manier om het te controleren is de app openen op een echt toestel, op een echte tekstschaal, in beide thema's, met een duim.

Waarom de agent daar uitkomt. Hij kan werkelijk niet zien. Hij genereert een plausibel scherm uit een beschrijving, zonder herinnering aan de negenendertig schermen ervoor en zonder beeld van het scherm dat hij net produceerde. Lokale plausibiliteit is het enige doel dat hem beschikbaar is.


6. ICU is niet optioneel, en aaneenplakken is geen vertalen

We bouwen apps voor markten waar dit fout doen zichtbaar is in de eerste zin van het eerste scherm. Agents doen het standaard fout.

Wat de agent doet.

Text('$count items');                                  // 1 items
Text('$count ${count == 1 ? "item" : "items"}');       // "clever", still broken
Text('Hello, ' + name + '!');                          // English word order, baked in
Text('${d.day}.${d.month}.${d.year}');                 // wrong half the world
Text('\$${amount.toStringAsFixed(2)}');                // wrong separator, wrong position

Het meervoud met een ternaire operator is de leerzame, want het ziet eruit alsof de developer erover heeft nagedacht. Het codeert een aanname die in het Engels waar is en vrijwel overal elders onwaar: dat een taal precies twee meervoudsvormen heeft. Het Russisch heeft vier categorieën (one, few, many, other) — 1 файл, 2 файла, 5 файлов — dus de ternaire operator is voor de meeste getallen op het scherm fout. Het Arabisch heeft er zes. Het Japans heeft er één, en überhaupt vervoegen leest als kapot. Geen enkele hoeveelheid geneste ternaire operatoren repareert dit, want de regel is data per taal, geen voorwaarde die je schrijft.

Hetzelfde geldt verderop. Aaneenplakken bakt Engelse zinsbouw in de broncode, zodat een vertaler de zin niet kan herordenen. Decimaalscheidingstekens, duizendtalscheidingstekens en de plaats van het valutasymbool zijn taalregiodata (1 234,56 € tegenover $1,234.56). Datumvolgorde is taalregiodata. En in RTL-lay-outs zet EdgeInsets.only(left: 16) — wat agents reflexmatig schrijven — de marge aan de verkeerde kant, waar EdgeInsetsDirectional.only(start: 16) correct was geweest.

Wat wij doen. Berichten leven in ARB-bestanden met ICU MessageFormat, gegenereerd tot getypeerde accessors door gen_l10n:

{
  "unreadMessages": "{count, plural, =0{No new messages} one{{count} new message} other{{count} new messages}}",
  "@unreadMessages": { "placeholders": { "count": { "type": "int" } } }
}

Het Russische ARB-bestand voor dezelfde sleutel draagt one/few/many/other — het formaat heeft ruimte voor de taal, en het framework kiest de juiste tak uit CLDR-data. NumberFormat.currency en DateFormat.yMMMd(locale) regelen de rest, en richtinggevoelige marges houden RTL eerlijk. De aanroepplek wordt Text(l10n.unreadMessages(count)), wat zowel korter is dan de ternaire operator als correct.

Waarom de agent daar uitkomt. Tekst interpoleren is de kortste weg naar letters op het scherm, en het is zichtbaar correct in de taal waarin de prompt is geschreven. De fout verschijnt pas in een taalregio waar de agent nooit naar is gevraagd, bij een getal dat hij nooit heeft geprobeerd.


7. Weten waar je vereenvoudigt — en waar niet

De laatste is oordeelsvermogen, en het is het lastigst over te dragen.

Wat de agent doet. Hij past vereenvoudiging uniform toe, wat in dezelfde codebase twee tegenovergestelde fouten oplevert.

Hij slaat complexiteit plat die dragend was:

try {
  await api.charge(order);
} catch (_) {
  // ingeslikt: netwerkfout, geweigerde kaart en idempotentieconflict
  // zijn nu dezelfde gebeurtenis, en de nieuwe poging incasseert dubbel
}

Logica voor opnieuw proberen en idempotentie wordt tot één aanroep samengeklapt omdat het "overbodig leek". Een race bij het vernieuwen van tokens krijgt een simpele await omdat de mutex "ongebruikt leek". Verschillende faalwijzen worden samengevoegd tot één Er ging iets mis, wat precies het bericht is dat een betalingsbug onreproduceerbaar maakt. Annuleringstokens sneuvelen omdat geen test ernaar verwees.

En hij abstraheert dingen te ver die prima waren: een AppButton met veertien optionele benoemde parameters en zes booleans, een generieke hiërarchie BaseRepository<T, ID> over drie endpoints, een AbstractBaseViewModel waar niemand van kan overerven zonder hem van begin tot eind te lezen, en de onvermijdelijke utils.dart waar veertig ongerelateerde statische functies sterven.

Wat wij doen. Complexiteit is een budget, en het wordt uitgegeven waar het domein werkelijk lastig is: betalingen, offline synchroniseren en conflicten oplossen, tokens vernieuwen, idempotentie, alles wat geld of identiteit raakt. Die plekken blijven met opzet expliciet, omslachtig en saai — elke faalwijze benoemd, elke herhaalpoging bewust. Overal elders klappen we stevig in: vijf bijna identieke lijstitems worden één widget, het instellingenscherm krijgt helemaal geen abstractielaag, en een repository die één endpoint omhult is gewoon een functie.

Waarom de agent daar uitkomt. "Welke delen van dit domein doen iemand pijn als ze fout zijn?" is geen informatie die in de code staat. Ze komt uit het product, de regelgevende context, en het aan de lijn hebben gezeten bij een incident. Een agent die op leesbare, minimale diffs optimaliseert behandelt een herhaallus voor betalingen en een instellingenschakelaar als hetzelfde soort ding, want syntactisch zijn ze dat.


Wat aansturen werkelijk betekent

Lees de zeven samen en de vorm is helder: elk daarvan is een eigenschap van het hele systeem — consistentie, dekking, een statemodel, een designsysteem, een interactiewoordenschat, een taalregiomodel, een complexiteitsbudget. Geen ervan kan prompt voor prompt ontstaan, want geen prompt ziet het geheel. Dat is geen gebrek dat je weg kunt prompten; het is wat het contextvenster is.

Dus verschuift het werk van de engineer omhoog, en het bestaat vooral uit vier dingen:

Zet de invarianten voordat de code bestaat. Het thema, het statemodel, de vertaalopzet, de mapindeling, de foutentaxonomie. Zodra die bestaan, heeft "volg het bestaande patroon" iets om naar te wijzen — en agents zijn werkelijk goed in een patroon volgen dat al in de repo staat. De eerste twee dagen bepalen de volgende zes maanden, en daarom krijgen ze een eigen fase in ons ontwikkelproces.

Bouw de terugkoppellussen die de agent niet heeft. Een strenge analysis_options.yaml met lints tot fouten verheven. flutter test met widget- en goldendekking. Een framebudget gecontroleerd op profielbuilds. Elk daarvan verandert een oordeel in een controle die de agent zelf kan draaien en waartegen hij kan itereren, en dat is de enige manier waarop zijn snelheid een voordeel wordt in plaats van een last.

Beoordeel besluiten, geen regels. Is dit hetzelfde statemodel als de rest van de app? Is dit vanuit het thema vormgegeven? Is dit de ene afleiding van die waarde? Overleeft deze tekst het Russisch? Regel voor regel agentuitvoer beoordelen schaalt niet en zal dat nooit doen; op besluitniveau beoordelen wel, want de besluiten zijn met weinig.

Open de app. Op een toestel, op 200% tekstgrootte, in donkere modus, in beide talen, met het netwerk geknepen. Dat kost tien minuten en vindt de hele categorie defecten die geen statische controle ooit meldt.

Dat is het werkelijke verschil tussen Flutter geschreven door een agent en Flutter geschreven door een team dat agents aanstuurt. Niet de code — de code is vaak prima. De randvoorwaarden eromheen.


Veelgestelde vragen

Ze kunnen goede Flutter-code schrijven binnen een codebase die al structuur heeft — een thema, een statemodel, een vertaalopzet, en tests om tegen te itereren. Wat ze niet kunnen is die structuur maken, want elk daarvan is een eigenschap van het hele systeem en een agent optimaliseert elke prompt lokaal. Met een bestaand patroon om te volgen is agentuitvoer werkelijk sterk. Met een lege repository en geen randvoorwaarden levert het veertig schermen op die elk werken en samen geen app vormen.
Omdat de agent geen herinnering heeft aan de schermen die hij eerder bouwde en geen manier om de app te zien die hij bouwt. Elk scherm wordt gegenereerd als plausibel losstaand antwoord, dus laadindicatoren, foutafhandeling, lege staten, bevestigingsdialogen en het moment van formuliervalidatie verschillen per scherm. Elke afzonderlijke keuze is verdedigbaar; het geheel leest als een app die uit losse tutorials is samengesteld, en gebruikers merken dat omdat ze een app maar één keer leren.
Een constante is lokaal correct en meteen bevredigend — de kleur verschijnt en het scherm bouwt. Een designsysteem door ThemeData rijgen betaalt zich pas terug over veel schermen en een tweede thema, en dat is precies de horizon die een optimalisator per prompt niet kan zien. De kosten komen later: donkere modus wordt een handmatige voorwaarde op elke aanroepplek, een merkwijziging wordt een diff van honderden bestanden, en tekstschaling van het systeem breekt de lay-out omdat groottes vaste getallen zijn in plaats van een typografische ladder.
Het codeert een aanname die in het Engels waar is en vrijwel overal elders onwaar: dat een taal precies twee meervoudsvormen heeft. Het Russisch heeft vier meervoudscategorieën, het Arabisch zes, en het Japans één. Een voorwaarde als aantal gelijk aan één kan geen van die uitdrukken. ICU MessageFormat in ARB-bestanden regelt meervoudscategorieën per taal op basis van CLDR-data, en hetzelfde geldt voor getalopmaak, de plaats van het valutasymbool, datumvolgorde en rechts-naar-links-lay-out, die geen van alle het aaneenplakken van tekenreeksen overleven.
Niet tenzij je erom vraagt. Een functie genereren en er tests voor genereren zijn twee aparte verzoeken en het tweede gebeurt meestal nooit, zodat code review de enige kwaliteitspoort wordt op precies het moment dat agents het volume code dat daar aankomt vermenigvuldigen. De oplossing met de grootste hefboom voor interfacewerk is goldentests, want een golden komt het dichtst bij een agent ogen geven: het verandert vragen als of een herontwerp de lege staat in donkere modus brak in een controle die in continue integratie in seconden draait.
Imperatieve vlaggen zijn het meest voorkomende patroon in trainingsdata en het makkelijkst regel voor regel te produceren, terwijl een stroom vraagt dat je een hele toestandsmachine tegelijk in gedachten houdt. Dus schrijven agents parallelle boolean- en nullable-velden en vertakken daaroverheen, waardoor onmogelijke combinaties weer te geven zijn en verouderde antwoorden kunnen winnen. Hetzelfde modelleren als een sealed stateklasse die uit een stroom wordt verbruikt laat de compiler volledigheid afdwingen en maakt vertragen en annuleren declaratief in plaats van met de hand gebouwd.
Meestal niet. De functies werken en het product is echt; wat ontbreekt is het bindweefsel — één bron van waarheid voor afgeleide state, een laag tests en goldens, een deugdelijk statemodel voor complexe asynchrone logica, een thema in plaats van constanten, een consistente interactiewoordenschat, en echte vertaling. Dat is allemaal ter plekke te herstellen, en dat gaat veel sneller dan een herbouw. Een gerichte audit wijst aan welke van de zeven het meest kost en in welke volgorde je ze herstelt.

Werk je aan een Flutter-app die een agent bouwde

Herken je je codebase in deze lijst, dan is dat geen teken dat AI gebruiken een vergissing was — het is hoe onbewaakte agentuitvoer er in Flutter elke keer uitziet, en het is te herstellen zonder herbouw.

Wij bouwen Flutter zo voor de kost: agents schrijven veel van de code, en onze engineers nemen de zeven besluiten voor hun rekening hierboven. Bekijk hoe wij Flutter-appontwikkeling doen, haal een bestaande codebase door een audit van AI-code, of plan een gratis analyse en we vertellen je welke van deze zeven je het meest kost.