Creare uno stepper personalizzato e wizard multi-step in Flutter

Foto di Yanhao Fang su Unsplash

GuideIntermedio35 min Flutter 3.x

Creare uno stepper personalizzato e wizard multi-step in Flutter

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.

In 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.

  1. 1

    Preparare lo scheletro dello Stepper

    Creiamo un StatefulWidget che ospita il nostro wizard. Il widget Stepper richiede:

    • currentStep: l'indice del passo attivo;
    • steps: la lista di oggetti Step;
    • onStepContinue e onStepCancel: callback per avanzare o tornare indietro.

    Partiamo con la struttura di base e tre passi vuoti.

    class RegistrationWizard extends StatefulWidget {
      const RegistrationWizard({super.key});
    
      @override
      State<RegistrationWizard> createState() => _RegistrationWizardState();
    }
    
    class _RegistrationWizardState extends State<RegistrationWizard> {
      int _currentStep = 0;
    
      void _onContinue() {
        if (_currentStep < 2) {
          setState(() => _currentStep++);
        }
      }
    
      void _onCancel() {
        if (_currentStep > 0) {
          setState(() => _currentStep--);
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Registrazione')),
          body: Stepper(
            currentStep: _currentStep,
            onStepContinue: _onContinue,
            onStepCancel: _onCancel,
            steps: const [
              Step(title: Text('Dati'), content: SizedBox.shrink()),
              Step(title: Text('Indirizzo'), content: SizedBox.shrink()),
              Step(title: Text('Conferma'), content: SizedBox.shrink()),
            ],
          ),
        );
      }
    }

    Risultato atteso

    Uno Stepper verticale con tre passi navigabili tramite i pulsanti Continua e Annulla.

  2. 2

    Aggiungere i form con validazione a ogni step

    Ogni passo deve avere il proprio Form con una GlobalKey<FormState> dedicata, così da poter validare separatamente i campi. Creiamo tre chiavi e i controller necessari, poi inseriamo i campi nel content di ciascuno Step.

    Usiamo una lista di GlobalKey per accedere allo stato del form corrispondente al passo attivo.

    final _formKeys = [
      GlobalKey<FormState>(),
      GlobalKey<FormState>(),
      GlobalKey<FormState>(),
    ];
    
    final _nameCtrl = TextEditingController();
    final _emailCtrl = TextEditingController();
    final _cityCtrl = TextEditingController();
    
    Step _buildDataStep() {
      return Step(
        title: const Text('Dati'),
        isActive: _currentStep >= 0,
        content: Form(
          key: _formKeys[0],
          child: Column(
            children: [
              TextFormField(
                controller: _nameCtrl,
                decoration: const InputDecoration(labelText: 'Nome'),
                validator: (v) =>
                    (v == null || v.isEmpty) ? 'Inserisci il nome' : null,
              ),
              TextFormField(
                controller: _emailCtrl,
                decoration: const InputDecoration(labelText: 'Email'),
                validator: (v) =>
                    (v != null && v.contains('@')) ? null : 'Email non valida',
              ),
            ],
          ),
        ),
      );
    }

    Risultato atteso

    Il primo passo mostra due campi di testo con validazione integrata.

  3. 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.

    Quando siamo all'ultimo passo, invece di avanzare, eseguiamo l'invio finale.

    void _onContinue() {
      final isValid = _formKeys[_currentStep].currentState?.validate() ?? false;
      if (!isValid) return;
    
      if (_currentStep < 2) {
        setState(() => _currentStep++);
      } else {
        _submit();
      }
    }
    
    void _submit() {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Benvenuto ${_nameCtrl.text}!')),
      );
    }

    Risultato atteso

    Non è possibile avanzare finché i campi del passo attivo non sono validi; all'ultimo step si conferma la registrazione.

  4. 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) {
      if (_currentStep > index) return StepState.complete;
      if (_currentStep == index) return StepState.editing;
      return StepState.indexed;
    }
    
    // dentro Stepper:
    controlsBuilder: (context, details) {
      final isLast = _currentStep == 2;
      return Padding(
        padding: const EdgeInsets.only(top: 16),
        child: Row(
          children: [
            FilledButton(
              onPressed: details.onStepContinue,
              child: Text(isLast ? 'Conferma' : 'Avanti'),
            ),
            const SizedBox(width: 8),
            if (_currentStep > 0)
              TextButton(
                onPressed: details.onStepCancel,
                child: const Text('Indietro'),
              ),
          ],
        ),
      );
    },

    Risultato atteso

    Ogni passo mostra un'icona di completamento e i pulsanti hanno etichette contestuali.

  5. 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.

    // dentro Stepper:
    onStepTapped: (index) {
      if (index < _currentStep) {
        setState(() => _currentStep = index);
      }
    },
    type: StepperType.vertical,

    Risultato atteso

    L'utente può tornare indietro toccando le intestazioni dei passi già completati, ma non saltare avanti.

  6. 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.

    Questo widget può essere posizionato sopra il contenuto del passo corrente, sostituendo del tutto lo Stepper standard.

    class StepIndicator extends StatelessWidget {
      final int current;
      final int total;
      const StepIndicator({super.key, required this.current, required this.total});
    
      @override
      Widget build(BuildContext context) {
        final color = Theme.of(context).colorScheme.primary;
        return Row(
          children: List.generate(total * 2 - 1, (i) {
            if (i.isOdd) {
              final done = (i ~/ 2) < current;
              return Expanded(
                child: Container(
                  height: 3,
                  color: done ? color : Colors.grey.shade300,
                ),
              );
            }
            final index = i ~/ 2;
            final active = index <= current;
            return CircleAvatar(
              radius: 16,
              backgroundColor: active ? color : Colors.grey.shade300,
              child: Text('${index + 1}',
                  style: TextStyle(
                      color: active ? Colors.white : Colors.black54)),
            );
          }),
        );
      }
    }

    Risultato atteso

    Un indicatore orizzontale con cerchi numerati e connettori colorati che segue il passo corrente.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!