Claude Code per l'iterazione UI Flutter: un ciclo pratico per trasformare user story in alberi di widget, stato e navigazione mantenendo i cambiamenti modulari e facili da revisionare.

Il lavoro rapido sull'interfaccia Flutter spesso parte bene. Modifichi un layout, aggiungi un pulsante, sposti un campo e lo schermo migliora in fretta. Il problema emerge dopo qualche giro, quando la velocità si tramuta in una pila di cambiamenti che nessuno vuole rivedere.
I team generalmente incontrano gli stessi fallimenti:
Una grande causa è l'approccio della “one big prompt”: descrivi l'intera feature, chiedi l'insieme completo di schermate e accetti un output vasto. L'assistente cerca di aiutare, ma tocca troppe parti del codice in una volta. Questo rende i cambiamenti disordinati, difficili da rivedere e rischiosi da mergiare.
Un loop ripetibile risolve questo forzando chiarezza e limitando il raggio d'azione. Invece di “costruire la feature”, procedi ripetutamente così: prendi una user story, genera la fetta di UI più piccola che la dimostri, aggiungi solo lo stato necessario per quella fetta, poi collega la navigazione per un solo percorso. Ogni passaggio resta sufficientemente piccolo da poter essere rivisto e gli errori sono facili da annullare.
L'obiettivo qui è un workflow pratico per trasformare le user story in schermate concrete, gestione dello stato e flussi di navigazione senza perdere il controllo. Fatto bene, ottieni pezzi di UI modulari, diff più piccoli e meno sorprese quando i requisiti cambiano.
Le user story sono scritte per esseri umani, non per alberi di widget. Prima di generare nulla, converti la story in una piccola specifica UI che descriva il comportamento visibile. “Done” dovrebbe essere testabile: ciò che l'utente può vedere, tappare e confermare, non se il design “sembra moderno”.
Un modo semplice per mantenere lo scope concreto è dividere la story in quattro bucket:
Se la story sembra ancora vaga, rispondi a queste domande in linguaggio semplice:
Aggiungi vincoli presto perché guidano ogni scelta di layout: basi del tema (colori, spaziatura, tipografia), reattività (prima telefono in portrait, poi larghezze tablet) e minimi di accessibilità come dimensione dei target di tap, scala del testo leggibile e etichette significative per le icone.
Infine, decidi cosa è stabile rispetto a cosa è flessibile così non fai churn nel codebase. Gli elementi stabili sono quelli da cui dipendono altre feature, come nomi di route, modelli di dati e API esistenti. Gli elementi flessibili sono più sicuri da iterare, come la struttura del layout, la microcopy e la composizione esatta dei widget.
Esempio: “Come utente, posso salvare un elemento tra i Preferiti dalla schermata di dettaglio.” Una specifica UI buildable potrebbe essere:
Questo è sufficiente per costruire, rivedere e iterare senza indovinare.
I diff piccoli non significano lavorare più lentamente. Rendono ogni cambiamento UI facile da rivedere, da annullare e difficile da rompere. La regola più semplice: una schermata o una interazione per iterazione.
Scegli una fetta stretta prima di iniziare. “Aggiungi uno stato vuoto alla schermata Ordini” è una buona fetta. “Rifare tutto il flusso Ordini” non lo è. Punta a un diff che un collega possa capire in un minuto.
Una struttura di cartelle stabile aiuta a mantenere i cambi contenuti. Un layout semplice, feature-first, impedisce di disperdere widget e rotte nell'app:
lib/
features/
orders/
screens/
widgets/
state/
routes.dart
Mantieni i widget piccoli e composti. Quando un widget ha input e output chiari, puoi cambiare layout senza toccare la logica di stato, e cambiare stato senza riscrivere la UI. Prediligi widget che prendono valori semplici e callback, non stato globale.
Un loop che resta revisionabile:
Imposta una regola ferma: ogni cambiamento deve essere facile da revertare o isolare. Evita refactor di passaggio mentre itera su una schermata. Se noti problemi non correlati, annotali e risolvili in un commit separato.
Se il tuo strumento supporta snapshot e rollback, usa ogni fetta come punto snapshot. Alcune piattaforme di vibe-coding come Koder.ai includono snapshot e rollback, che possono rendere l'esperimento più sicuro quando provi cambi UI audaci.
Un'altra abitudine che mantiene calme le prime iterazioni: preferisci aggiungere nuovi widget piuttosto che modificare quelli condivisi. I componenti condivisi sono dove i piccoli cambi diventano grandi diff.
Il lavoro UI veloce resta sicuro quando separi il pensiero dalla digitazione. Inizia ottenendo un piano chiaro dell'albero dei widget prima di generare codice.
Chiedi solo un outline dell'albero dei widget. Vuoi i nomi dei widget, la gerarchia e cosa mostra ciascuna parte. Niente codice ancora. Qui intercetti stati mancanti, schermate vuote e scelte di layout strane mentre tutto è ancora economico da cambiare.
Chiedi una ripartizione dei componenti con responsabilità. Mantieni ogni widget focalizzato: un widget renderizza l'header, un altro la lista, un altro gestisce empty/error UI. Se qualcosa avrà bisogno di stato dopo, annotalo ora ma non implementarlo ancora.
Genera lo scaffold della schermata e i widget stateless. Parti con un singolo file di schermata con contenuto placeholder e TODO chiari. Mantieni gli input espliciti (parametri del costruttore) così puoi collegare lo stato reale dopo senza riscrivere l'albero.
Fai un pass separato per styling e dettagli di layout: spaziatura, tipografia, theming e comportamento responsive. Tratta lo styling come un diff a parte così le review restano semplici.
Metti i vincoli in testa così l'assistente non inventa UI che non puoi spedire:
Esempio concreto: la user story è “Come utente, posso rivedere i miei elementi salvati e rimuoverne uno.” Chiedi un albero di widget che includa una app bar, una lista con righe elemento e uno stato vuoto. Poi richiedi una ripartizione come SavedItemsScreen, SavedItemTile, EmptySavedItems. Solo dopo, genera lo scaffold con widget stateless e dati finti, e infine aggiungi lo styling (divider, padding e un pulsante rimuovi chiaro) in un pass separato.
L'iterazione UI si sfalda quando ogni widget comincia a prendere decisioni. Mantieni l'albero dei widget “stupido”: deve leggere lo stato e renderizzare, non contenere regole di business.
Inizia nominando gli stati in parole semplici. La maggior parte delle feature necessita di più di «loading» e «done»:
Poi elenca gli eventi che possono cambiare lo stato: tap, submit del form, pull-to-refresh, back, retry e “utente ha modificato un campo”. Farlo in anticipo evita congetture più tardi.
Scegli un approccio di stato per la feature e mantienilo. L'obiettivo non è “il pattern migliore”, ma diff coerenti.
Per una schermata piccola, un semplice controller (come ChangeNotifier o ValueNotifier) è spesso sufficiente. Metti la logica in un unico posto:
Prima di aggiungere codice, scrivi le transizioni di stato in inglese semplice. Esempio per una schermata di login:
"Quando l'utente preme Sign in: imposta Loading. Se l'email è invalida: resta in Partial input e mostra un messaggio inline. Se la password è sbagliata: imposta Error con messaggio e abilita Retry. Se successo: imposta Success e naviga a Home."
Poi genera il codice Dart minimale che corrisponde a quelle frasi. Le review restano semplici perché puoi confrontare il diff con le regole.
Rendi la validazione esplicita. Decidi cosa succede quando gli input sono invalidi:
Quando queste risposte sono scritte, la UI resta pulita e il codice di stato rimane piccolo.
Una buona navigazione inizia come una mappa piccola, non come un cumulo di rotte. Per ogni user story, scrivi quattro momenti: dove entra l'utente, il passo successivo più probabile, come annulla e cosa significa “back” (ritorno alla schermata precedente o a uno stato home sicuro).
Una semplice mappa delle rotte dovrebbe rispondere alle domande che solitamente causano rifacimenti:
Poi definisci i parametri passati tra le schermate. Sii esplicito: ID (productId, orderId), filtri (intervallo date, stato) e dati bozza (un form parzialmente compilato). Se salti questo, finirai per infilare stato in singleton globali o ricostruire schermate per “trovare” il contesto.
I deep link contano anche se non li spedisci il primo giorno. Decidi cosa succede quando un utente atterra a metà flusso: puoi caricare i dati mancanti o dovresti reindirizzare a una schermata di ingresso sicura?
Decidi anche quali schermate devono ritornare risultati. Esempio: una schermata "Select Address" restituisce un addressId e la schermata checkout si aggiorna senza un refresh completo. Mantieni la forma del risultato piccola e tipata così i cambi restano facili da rivedere.
Prima di codare, indica i casi limite: modifiche non salvate (mostra dialog di conferma), auth richiesta (pausa e riprendi dopo il login) e dati mancanti o cancellati (mostra errore e una via d'uscita chiara).
Quando iteri velocemente, il vero rischio non è "UI sbagliata." È UI non revisionabile. Se un collega non capisce cosa è cambiato, perché è cambiato e cosa è rimasto stabile, ogni iterazione successiva rallenta.
Una regola che aiuta: blocca prima le interfacce, poi lascia muovere gli internals. Stabilizza le props pubbliche dei widget (input), i piccoli modelli UI e gli argomenti di rotta. Una volta nominati e tipati, puoi rimodellare l'albero dei widget senza rompere il resto dell'app.
Chiedi un piano diff-friendly prima di generare codice. Vuoi un piano che dica quali file cambieranno e quali devono restare intatti. Questo mantiene le review focalizzate e previene refactor accidentali che cambiano il comportamento.
Pattern che mantengono i diff piccoli:
Se la user story è “Come acquirente, posso modificare il mio indirizzo di spedizione dal checkout.” Blocca prima gli argomenti di rotta: CheckoutArgs(cartId, shippingAddressId) resta stabile. Poi itera dentro la schermata. Quando il layout si stabilizza, dividilo in AddressForm, AddressSummary e SaveBar.
Se la gestione dello stato cambia (per esempio la validazione si sposta dal widget in un CheckoutController), la review resta leggibile: i file UI cambiano principalmente nel rendering, mentre il controller mostra la logica cambiata in un unico posto.
Il modo più rapido per rallentare è chiedere all'assistente di cambiare tutto in una volta. Se un commit tocca layout, stato e navigazione, i reviewer non sapranno cosa ha rotto e il rollback diventa complicato.
Un'abitudine più sicura è un intento per iterazione: definisci l'albero widget, poi collega lo stato, poi la navigazione.
Un problema comune è lasciare che il codice generato inventi un pattern nuovo per ogni schermata. Se una pagina usa Provider, la successiva usa setState e la terza introduce una classe controller custom, l'app diventa incoerente rapidamente. Scegli un piccolo set di pattern e applicali.
Un altro errore è mettere lavoro async direttamente dentro build(). Può sembrare OK in una demo veloce, ma scatena chiamate ripetute ai rebuild, flicker e bug difficili da tracciare. Sposta la chiamata in initState(), in un view model o in un controller dedicato e tieni build() concentrato sul rendering.
Il naming è una trappola silenziosa. Codice che compila ma si legge come Widget1, data2 o temp rende i refactor futuri dolorosi. Nomi chiari aiutano anche l'assistente a produrre cambi successivi migliori perché l'intento è ovvio.
Guardrail che prevengono i peggiori esiti:
build()Una classica correzione visiva è aggiungere un altro Container, Padding, Align e SizedBox finché non sembra giusto. Dopo qualche passaggio, l'albero diventa illeggibile.
Se un pulsante è disallineato, prova prima a rimuovere wrapper, usare un singolo widget layout genitore o estrarre un piccolo widget con i propri vincoli.
Esempio: una schermata checkout dove il prezzo totale salta durante il loading. Un assistente potrebbe avvolgere la riga del prezzo in più widget per “stabilizzarla”. Una correzione più pulita è riservare spazio con un placeholder di loading mantenendo la struttura della riga invariata.
Prima di fare commit, fai un pass di due minuti che verifica il valore per l'utente e ti protegge da regressioni inattese. L'obiettivo non è la perfezione. È fare in modo che questa iterazione sia facile da rivedere, testare e annullare.
Leggi la user story una volta, poi verifica questi elementi contro l'app in esecuzione (o almeno contro un semplice widget test):
Un check di realtà rapido: se hai aggiunto una nuova schermata Dettagli Ordine, dovresti poter (1) aprirla dalla lista, (2) vedere uno spinner di loading, (3) simulare un errore, (4) vedere un ordine vuoto e (5) premere back per tornare alla lista senza salti strani.
Se il tuo workflow supporta snapshot e rollback, scatta uno snapshot prima di cambi UI più grandi. Alcune piattaforme come Koder.ai lo supportano e possono aiutarti a iterare più veloce senza mettere a rischio il ramo principale.
User story: "Come acquirente, posso sfogliare articoli, aprire una pagina dettagli, salvare un articolo tra i preferiti e poi vedere i miei preferiti." L'obiettivo è passare dalle parole alle schermate in tre piccoli passi revisionabili.
Iterazione 1: concentrati solo sulla schermata di browse. Crea un albero widget abbastanza completo da renderizzare ma non collegato a dati reali: uno Scaffold con una AppBar, una ListView di righe placeholder e UI chiare per loading e empty. Mantieni lo stato semplice: loading (mostra CircularProgressIndicator), empty (mostra un breve messaggio e magari un pulsante Try again) e ready (mostra la lista).
Iterazione 2: aggiungi la schermata dettagli e la navigazione. Mantieni esplicito: onTap fa push di una rotta e passa un piccolo oggetto param (ad esempio: item id, title). Inizia la pagina dettaglio in sola lettura con un titolo, una descrizione placeholder e un pulsante Favorite. L'obiettivo è soddisfare la story: lista -> dettaglio -> back, senza flussi extra.
Iterazione 3: introduci aggiornamenti dello stato dei preferiti e feedback UI. Aggiungi una singola fonte di verità per i preferiti (anche in memoria è ok) e collegala a entrambe le schermate. Il tap su Favorite aggiorna immediatamente l'icona e mostra una piccola conferma (es. SnackBar). Poi aggiungi una schermata Favorites che legge lo stesso stato e gestisce lo stato vuoto.
Un diff revisionabile tipicamente assomiglia a:
browse_list_screen.dart: albero widget più UI loading/empty/readyitem_details_screen.dart: layout UI e accetta param di navigazionefavorites_store.dart: contenitore stato minimale e metodi di updateapp_routes.dart: rotte e helper di navigazione tipatifavorites_screen.dart: legge lo stato e mostra UI empty/listSe un file diventa “il posto dove succede tutto”, dividilo prima di procedere. File piccoli con nomi chiari mantengono l'iterazione successiva veloce e sicura.
Se il workflow funziona solo quando sei “in the zone”, si romperà nel momento in cui cambi schermata o un collega tocca la feature. Rendi il loop un'abitudine scrivendolo e mettendo guardrail intorno alla dimensione dei cambi.
Usa un template di team in modo che ogni iterazione parta con gli stessi input e produca lo stesso tipo di output. Mantienilo breve ma specifico:
Questo riduce la probabilità che l'assistente inventi pattern nuovi a metà feature.
Scegli una definizione di piccolo facile da far rispettare in code review. Per esempio, limita ogni iterazione a un numero ridotto di file e separa i refactor UI dai cambi di comportamento.
Un set semplice di regole:
Aggiungi checkpoint così puoi annullare rapidamente un passo sbagliato. Al minimo, tagga commit o tieni checkpoint locali prima di refactor importanti. Se il workflow supporta snapshot e rollback, usali aggressivamente.
Se vuoi un workflow basato su chat che possa generare e rifinire app Flutter end-to-end, Koder.ai include una modalità di pianificazione che aiuta a rivedere un piano e i file previsti prima di applicarli.
Usa una specifica UI piccola e verificabile prima di tutto. Scrivi 3–6 righe che coprano:
Poi costruisci solo quella fetta (spesso una schermata + 1–2 widget).
Converti la user story in quattro insiemi:
Se non riesci a descrivere rapidamente il check di accettazione, la story è ancora troppo vaga per una diff pulita.
Inizia generando solo l'albero dei widget (nomi + gerarchia + cosa mostra ogni parte). Niente codice.
Poi richiedi una ripartizione delle responsabilità dei componenti (cosa possiede ciascun widget).
Solo dopo genera lo scaffold stateless con input espliciti (valori + callback) e fai lo styling in un pass separato.
Trattalo come una regola ferrea: un intento per iterazione.
Se un singolo commit cambia layout, stato e rotte insieme, i reviewer non sapranno cosa ha causato un bug e il rollback diventa complicato.
Tieni i widget “stupidi”: devono renderizzare lo stato, non prendere decisioni di business.
Un default pratico:
Evita chiamate async dentro —portano a ripetute invocazioni ad ogni rebuild.
Definisci stati e transizioni in chiaro prima di scrivere codice.
Esempio di pattern:
Poi elenca gli eventi che muovono tra questi stati (refresh, retry, submit, edit). Il codice sarà più facile da confrontare con le regole scritte.
Scrivi una piccola “mappa del flusso” per la story:
Default su cartelle feature-first così i cambi restano contenuti. Per esempio:
lib/features/<feature>/screens/lib/features/<feature>/widgets/lib/features/<feature>/state/lib/features/<feature>/routes.dartPoi mantieni ogni iterazione concentrata sulla cartella della feature ed evita refactor casuali altrove.
Una regola semplice: stabilizza le interfacce, non gli internals.
I reviewer apprezzano che input/output rimangano stabili anche se la struttura interna cambia.
Fai un rapido controllo di due minuti:
Se il tuo workflow lo supporta (es. snapshot/rollback), scatta uno snapshot prima di un refactor di layout più grande così puoi tornare indietro in sicurezza.
build()Blocca anche cosa passa tra le schermate (ID, filtri, dati bozza) così non finisci a nascondere il contesto in globali.