[{"data":1,"prerenderedAt":73},["ShallowReactive",2],{"tutorial-creare-uno-stepper-personalizzato-e-wizard-multi-step-in-flutter":3,"comments-tutorial-creare-uno-stepper-personalizzato-e-wizard-multi-step-in-flutter":72},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":15,"difficulty":16,"estimated_minutes":17,"flutter_version":18,"status":19,"published_at":20,"meta_title":21,"meta_description":22,"category":23,"author":27,"steps":29},59,"Creare uno stepper personalizzato e wizard multi-step in Flutter","creare-uno-stepper-personalizzato-e-wizard-multi-step-in-flutter","Impara a costruire un processo guidato a più fasi in Flutter usando il widget Stepper, gestendo validazione, navigazione tra i passi e uno stepper orizzontale personalizzato.","Molte applicazioni richiedono di guidare l'utente attraverso un processo suddiviso in fasi: checkout, onboarding con più form, configurazione di un account, ecc. Flutter offre il widget `Stepper`, ma con qualche accortezza possiamo trasformarlo in un wizard robusto con validazione per ogni passo e uno stile personalizzato.\n\nIn questo tutorial costruiremo un wizard di registrazione a tre fasi (dati personali, indirizzo, conferma), gestendo lo stato dei form, il passaggio da uno step all'altro e la validazione. Alla fine vedremo anche come realizzare un indicatore di passi orizzontale personalizzato quando `Stepper` non basta.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F31a7c6ec-ec0e-4379-aafb-55906ff70a82.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1772602484656-5ef8225fde19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODczMjcyOTR8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Yanhao Fang","https:\u002F\u002Funsplash.com\u002F@alamanga","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fperson-with-camera-and-high-heel-shoe-on-steps-Zzie2rYg1h0",null,"intermediate",35,"3.x","published","2026-08-10T04:30:45+00:00","Stepper e wizard multi-step in Flutter","Guida pratica per creare un wizard a più fasi in Flutter con Stepper, validazione dei form per ogni passo e un indicatore di step personalizzato.",{"id":24,"name":25,"slug":26},1,"Guide","guide",{"id":24,"name":28},"Flutter Bot",[30,37,44,51,58,65],{"id":31,"position":24,"title":32,"body":33,"code_snippet":34,"code_language":35,"expected_result":36,"demo_url":15,"video_url":15},396,"Preparare lo scheletro dello Stepper","Creiamo un `StatefulWidget` che ospita il nostro wizard. Il widget `Stepper` richiede:\n\n- `currentStep`: l'indice del passo attivo;\n- `steps`: la lista di oggetti `Step`;\n- `onStepContinue` e `onStepCancel`: callback per avanzare o tornare indietro.\n\nPartiamo con la struttura di base e tre passi vuoti.","class RegistrationWizard extends StatefulWidget {\n  const RegistrationWizard({super.key});\n\n  @override\n  State\u003CRegistrationWizard> createState() => _RegistrationWizardState();\n}\n\nclass _RegistrationWizardState extends State\u003CRegistrationWizard> {\n  int _currentStep = 0;\n\n  void _onContinue() {\n    if (_currentStep \u003C 2) {\n      setState(() => _currentStep++);\n    }\n  }\n\n  void _onCancel() {\n    if (_currentStep > 0) {\n      setState(() => _currentStep--);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Registrazione')),\n      body: Stepper(\n        currentStep: _currentStep,\n        onStepContinue: _onContinue,\n        onStepCancel: _onCancel,\n        steps: const [\n          Step(title: Text('Dati'), content: SizedBox.shrink()),\n          Step(title: Text('Indirizzo'), content: SizedBox.shrink()),\n          Step(title: Text('Conferma'), content: SizedBox.shrink()),\n        ],\n      ),\n    );\n  }\n}","dart","Uno Stepper verticale con tre passi navigabili tramite i pulsanti Continua e Annulla.",{"id":38,"position":39,"title":40,"body":41,"code_snippet":42,"code_language":35,"expected_result":43,"demo_url":15,"video_url":15},397,2,"Aggiungere i form con validazione a ogni step","Ogni passo deve avere il proprio `Form` con una `GlobalKey\u003CFormState>` dedicata, così da poter validare separatamente i campi. Creiamo tre chiavi e i controller necessari, poi inseriamo i campi nel `content` di ciascuno `Step`.\n\nUsiamo una lista di `GlobalKey` per accedere allo stato del form corrispondente al passo attivo.","final _formKeys = [\n  GlobalKey\u003CFormState>(),\n  GlobalKey\u003CFormState>(),\n  GlobalKey\u003CFormState>(),\n];\n\nfinal _nameCtrl = TextEditingController();\nfinal _emailCtrl = TextEditingController();\nfinal _cityCtrl = TextEditingController();\n\nStep _buildDataStep() {\n  return Step(\n    title: const Text('Dati'),\n    isActive: _currentStep >= 0,\n    content: Form(\n      key: _formKeys[0],\n      child: Column(\n        children: [\n          TextFormField(\n            controller: _nameCtrl,\n            decoration: const InputDecoration(labelText: 'Nome'),\n            validator: (v) =>\n                (v == null || v.isEmpty) ? 'Inserisci il nome' : null,\n          ),\n          TextFormField(\n            controller: _emailCtrl,\n            decoration: const InputDecoration(labelText: 'Email'),\n            validator: (v) =>\n                (v != null && v.contains('@')) ? null : 'Email non valida',\n          ),\n        ],\n      ),\n    ),\n  );\n}","Il primo passo mostra due campi di testo con validazione integrata.",{"id":45,"position":46,"title":47,"body":48,"code_snippet":49,"code_language":35,"expected_result":50,"demo_url":15,"video_url":15},398,3,"Validare prima di passare al passo successivo","Modifichiamo `_onContinue` per validare il form del passo corrente prima di avanzare. Se la validazione fallisce, restiamo sullo step attuale e mostriamo gli errori.\n\nQuando siamo all'ultimo passo, invece di avanzare, eseguiamo l'invio finale.","void _onContinue() {\n  final isValid = _formKeys[_currentStep].currentState?.validate() ?? false;\n  if (!isValid) return;\n\n  if (_currentStep \u003C 2) {\n    setState(() => _currentStep++);\n  } else {\n    _submit();\n  }\n}\n\nvoid _submit() {\n  ScaffoldMessenger.of(context).showSnackBar(\n    SnackBar(content: Text('Benvenuto ${_nameCtrl.text}!')),\n  );\n}","Non è possibile avanzare finché i campi del passo attivo non sono validi; all'ultimo step si conferma la registrazione.",{"id":52,"position":53,"title":54,"body":55,"code_snippet":56,"code_language":35,"expected_result":57,"demo_url":15,"video_url":15},399,4,"Personalizzare stato ed etichette dei pulsanti","Rendiamo il wizard più chiaro impostando lo `state` di ogni `Step` (completato, attivo, di errore) e personalizzando i pulsanti tramite `controlsBuilder`. Cambiamo l'etichetta del pulsante nell'ultimo passo in 'Conferma'.","StepState _stateFor(int index) {\n  if (_currentStep > index) return StepState.complete;\n  if (_currentStep == index) return StepState.editing;\n  return StepState.indexed;\n}\n\n\u002F\u002F dentro Stepper:\ncontrolsBuilder: (context, details) {\n  final isLast = _currentStep == 2;\n  return Padding(\n    padding: const EdgeInsets.only(top: 16),\n    child: Row(\n      children: [\n        FilledButton(\n          onPressed: details.onStepContinue,\n          child: Text(isLast ? 'Conferma' : 'Avanti'),\n        ),\n        const SizedBox(width: 8),\n        if (_currentStep > 0)\n          TextButton(\n            onPressed: details.onStepCancel,\n            child: const Text('Indietro'),\n          ),\n      ],\n    ),\n  );\n},","Ogni passo mostra un'icona di completamento e i pulsanti hanno etichette contestuali.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":35,"expected_result":64,"demo_url":15,"video_url":15},400,5,"Toccare direttamente un passo per navigare","Il parametro `onStepTapped` permette di saltare a un passo cliccando sull'intestazione. Consentiamo la navigazione solo verso passi già completati, per evitare di saltare la validazione dei form precedenti.","\u002F\u002F dentro Stepper:\nonStepTapped: (index) {\n  if (index \u003C _currentStep) {\n    setState(() => _currentStep = index);\n  }\n},\ntype: StepperType.vertical,","L'utente può tornare indietro toccando le intestazioni dei passi già completati, ma non saltare avanti.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":35,"expected_result":71,"demo_url":15,"video_url":15},401,6,"Creare un indicatore di step orizzontale personalizzato","Quando `Stepper` non offre abbastanza flessibilità grafica, possiamo costruire un indicatore orizzontale personalizzato: una `Row` di cerchi numerati collegati da linee, con colori che riflettono lo stato del passo.\n\nQuesto widget può essere posizionato sopra il contenuto del passo corrente, sostituendo del tutto lo `Stepper` standard.","class StepIndicator extends StatelessWidget {\n  final int current;\n  final int total;\n  const StepIndicator({super.key, required this.current, required this.total});\n\n  @override\n  Widget build(BuildContext context) {\n    final color = Theme.of(context).colorScheme.primary;\n    return Row(\n      children: List.generate(total * 2 - 1, (i) {\n        if (i.isOdd) {\n          final done = (i ~\u002F 2) \u003C current;\n          return Expanded(\n            child: Container(\n              height: 3,\n              color: done ? color : Colors.grey.shade300,\n            ),\n          );\n        }\n        final index = i ~\u002F 2;\n        final active = index \u003C= current;\n        return CircleAvatar(\n          radius: 16,\n          backgroundColor: active ? color : Colors.grey.shade300,\n          child: Text('${index + 1}',\n              style: TextStyle(\n                  color: active ? Colors.white : Colors.black54)),\n        );\n      }),\n    );\n  }\n}","Un indicatore orizzontale con cerchi numerati e connettori colorati che segue il passo corrente.",[],1789120602644]