Navigazione di base in Flutter con Navigator: push, pop e passaggio dati

Foto di Marjan Blan su Unsplash

GuidePrincipiante30 min Flutter 3.x

Navigazione di base in Flutter con Navigator: push, pop e passaggio dati

Quasi ogni app ha più di una schermata: una lista di prodotti che apre il dettaglio, un profilo che apre le impostazioni, un pulsante che porta a un form.

In Flutter la navigazione è gestita dal widget Navigator, che mantiene uno stack (una pila) di schermate: push mette una nuova schermata sopra la pila, pop la toglie e torna a quella precedente.

In questa guida per principianti vedrai:

  • come aprire una nuova schermata con Navigator.push e MaterialPageRoute;
  • come tornare indietro con Navigator.pop (e cosa fa il tasto back di Android);
  • come passare dati alla schermata di destinazione;
  • come ricevere un risultato dalla schermata aperta;
  • come usare le rotte con nome (routes e pushNamed) per progetti più ordinati.

Non servono pacchetti esterni: tutto quello che useremo è già incluso in Flutter. Se in futuro ti servirà una navigazione più avanzata (deep link, URL sul web) potrai passare a go_router, ma i concetti di questa guida restano validi.

  1. 1

    Preparare il progetto e la prima schermata

    Crea un nuovo progetto (o parti da uno esistente) e svuota il file lib/main.dart.

    Partiamo da una HomeScreen molto semplice, con uno Scaffold, una AppBar e un pulsante centrale che per ora non fa nulla.

    Punti chiave:

    • MaterialApp contiene già al suo interno un Navigator: è questo che ci permette di usare Navigator.push in qualsiasi punto dell'app;
    • home: indica la prima rotta della pila, quella che l'utente vede all'avvio.

    Avvia l'app con flutter run: dovresti vedere la home con il pulsante.

    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Navigazione base',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            useMaterial3: true,
          ),
          home: const HomeScreen(),
        );
      }
    }
    
    class HomeScreen extends StatelessWidget {
      const HomeScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Center(
            child: FilledButton(
              onPressed: () {
                // Qui apriremo la seconda schermata
              },
              child: const Text('Vai al dettaglio'),
            ),
          ),
        );
      }
    }

    Risultato atteso

    L'app si avvia mostrando la schermata Home con un pulsante "Vai al dettaglio" al centro, che al tocco non fa ancora nulla.

  2. 2

    Aprire una seconda schermata con Navigator.push

    Creiamo una seconda schermata, DettaglioScreen, e apriamola con Navigator.push.

    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const DettaglioScreen()),
    );
    

    Cosa succede:

    • context serve a Flutter per trovare il Navigator più vicino risalendo l'albero dei widget;
    • MaterialPageRoute descrive la nuova rotta e fornisce l'animazione di transizione tipica della piattaforma (slide su iOS, fade/scale su Android);
    • il parametro builder costruisce la schermata solo quando serve.

    Nota che nella AppBar della seconda schermata compare automaticamente la freccia "indietro": Flutter la aggiunge da solo quando c'è una rotta sottostante nella pila.

    class HomeScreen extends StatelessWidget {
      const HomeScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Center(
            child: FilledButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const DettaglioScreen(),
                  ),
                );
              },
              child: const Text('Vai al dettaglio'),
            ),
          ),
        );
      }
    }
    
    class DettaglioScreen extends StatelessWidget {
      const DettaglioScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Dettaglio')),
          body: const Center(
            child: Text('Sono la seconda schermata'),
          ),
        );
      }
    }

    Risultato atteso

    Toccando il pulsante si apre la schermata Dettaglio con animazione di transizione e freccia "indietro" nella AppBar.

  3. 3

    Tornare indietro con Navigator.pop

    Il tasto back di Android e la freccia della AppBar chiamano già Navigator.pop per te. Ma spesso serve un pulsante esplicito, ad esempio "Annulla" o "Chiudi".

    Navigator.pop(context);
    

    pop rimuove la rotta corrente dalla pila e mostra quella sotto.

    Errore tipico da principiante: chiamare Navigator.pop(context) sulla prima schermata. La pila resta vuota e su Android l'app si chiude (su iOS non succede nulla). Puoi verificare se è possibile tornare indietro con Navigator.canPop(context).

    class DettaglioScreen extends StatelessWidget {
      const DettaglioScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Dettaglio')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('Sono la seconda schermata'),
                const SizedBox(height: 16),
                OutlinedButton(
                  onPressed: () {
                    if (Navigator.canPop(context)) {
                      Navigator.pop(context);
                    }
                  },
                  child: const Text('Torna indietro'),
                ),
              ],
            ),
          ),
        );
      }
    }

    Risultato atteso

    Il pulsante "Torna indietro" riporta alla Home esattamente come la freccia della AppBar.

  4. 4

    Passare dati alla schermata di destinazione

    Il modo più semplice e sicuro per passare dati è tramite il costruttore del widget di destinazione: niente stringhe magiche, tutto controllato dal compilatore.

    Creiamo un piccolo modello Prodotto e una lista nella Home: toccando un elemento apriamo il dettaglio passando l'oggetto selezionato.

    Osserva:

    • il costruttore di DettaglioScreen riceve un parametro required this.prodotto;
    • il campo è final, quindi immutabile;
    • non c'è nessun cast o controllo di tipo da fare: se sbagli tipo, l'errore esce in fase di compilazione.
    class Prodotto {
      final String nome;
      final double prezzo;
    
      const Prodotto(this.nome, this.prezzo);
    }
    
    const prodotti = <Prodotto>[
      Prodotto('Tastiera meccanica', 89.90),
      Prodotto('Mouse wireless', 34.50),
      Prodotto('Monitor 27"', 219.00),
    ];
    
    class HomeScreen extends StatelessWidget {
      const HomeScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Prodotti')),
          body: ListView.builder(
            itemCount: prodotti.length,
            itemBuilder: (context, index) {
              final prodotto = prodotti[index];
              return ListTile(
                title: Text(prodotto.nome),
                subtitle: Text('${prodotto.prezzo.toStringAsFixed(2)} €'),
                trailing: const Icon(Icons.chevron_right),
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => DettaglioScreen(prodotto: prodotto),
                    ),
                  );
                },
              );
            },
          ),
        );
      }
    }
    
    class DettaglioScreen extends StatelessWidget {
      const DettaglioScreen({super.key, required this.prodotto});
    
      final Prodotto prodotto;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text(prodotto.nome)),
          body: Center(
            child: Text(
              'Prezzo: ${prodotto.prezzo.toStringAsFixed(2)} €',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
          ),
        );
      }
    }

    Risultato atteso

    La Home mostra la lista dei prodotti; toccando una riga si apre il dettaglio con nome e prezzo dell'elemento selezionato.

  5. 5

    Ricevere un risultato dalla schermata aperta

    Navigator.push restituisce un Future che si completa quando la schermata viene chiusa. Il valore restituito è quello passato a Navigator.pop(context, valore).

    È il pattern perfetto per schermate di selezione o conferma:

    1. la Home fa await Navigator.push<bool>(...);
    2. il dettaglio chiama Navigator.pop(context, true);
    3. la Home riceve true e mostra una SnackBar.

    Attenzione: il risultato può essere null se l'utente torna indietro con il tasto di sistema o la freccia della AppBar. Gestisci sempre questo caso.

    Un altro punto importante: dopo un await il widget potrebbe non essere più nell'albero. Per questo si controlla if (!context.mounted) return; prima di usare di nuovo il context.

    // Nella HomeScreen, dentro onTap del ListTile
    onTap: () async {
      final aggiunto = await Navigator.push<bool>(
        context,
        MaterialPageRoute(
          builder: (context) => DettaglioScreen(prodotto: prodotto),
        ),
      );
    
      if (!context.mounted) return;
    
      if (aggiunto == true) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${prodotto.nome} aggiunto al carrello')),
        );
      }
    },
    
    // Nella DettaglioScreen
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('Prezzo: ${prodotto.prezzo.toStringAsFixed(2)} €'),
          const SizedBox(height: 16),
          FilledButton.icon(
            onPressed: () => Navigator.pop(context, true),
            icon: const Icon(Icons.add_shopping_cart),
            label: const Text('Aggiungi al carrello'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Annulla'),
          ),
        ],
      ),
    ),

    Risultato atteso

    Toccando "Aggiungi al carrello" si torna alla Home e compare una SnackBar di conferma; con "Annulla" o con il back non compare nulla.

  6. 6

    Usare le rotte con nome: routes e pushNamed

    Quando le schermate crescono, elencarle tutte in MaterialApp rende il codice più ordinato e permette di navigare senza importare ogni volta la classe della schermata.

    Si definisce la mappa routes e si usa Navigator.pushNamed(context, '/impostazioni').

    Per passare dati con le rotte con nome si usa arguments, recuperabile con ModalRoute.of(context)!.settings.arguments. Attenzione: qui il tipo non è controllato dal compilatore, quindi serve un cast (as Prodotto). Per questo, per i dati complessi, il costruttore visto nel passo 4 resta l'approccio più sicuro.

    Altri metodi utili da conoscere:

    • Navigator.pushReplacementNamed → sostituisce la rotta corrente (tipico dopo il login, per non tornare alla schermata di login);
    • Navigator.popUntil(context, (route) => route.isFirst) → torna alla prima schermata chiudendo tutte le altre.
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          initialRoute: '/',
          routes: {
            '/': (context) => const HomeScreen(),
            '/impostazioni': (context) => const ImpostazioniScreen(),
          },
        );
      }
    }
    
    // Apertura della rotta con nome
    IconButton(
      icon: const Icon(Icons.settings),
      onPressed: () => Navigator.pushNamed(context, '/impostazioni'),
    ),
    
    class ImpostazioniScreen extends StatelessWidget {
      const ImpostazioniScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Impostazioni')),
          body: Center(
            child: TextButton(
              onPressed: () => Navigator.popUntil(
                context,
                (route) => route.isFirst,
              ),
              child: const Text('Torna alla Home'),
            ),
          ),
        );
      }
    }

    Risultato atteso

    L'icona ingranaggio nella AppBar apre la schermata Impostazioni; il pulsante "Torna alla Home" riporta alla prima rotta della pila.

  7. 7

    Errori frequenti e buone pratiche

    Prima di chiudere, i problemi che incontrerai quasi sicuramente:

    1. Navigator operation requested with a context that does not include a Navigator

    Succede quando usi il context del widget che contiene MaterialApp: in quel punto il Navigator non esiste ancora. Soluzione: estrai la home in un widget separato (come HomeScreen) oppure usa un Builder.

    2. Usare il context dopo un await

    Dopo un'operazione asincrona il widget potrebbe essere stato smontato. Controlla sempre context.mounted (o mounted in uno State).

    3. Dimenticare che pop può restituire null

    Se l'utente esce con il back di sistema, il risultato è null: gestisci il caso con un valore di default o un if.

    4. Impilare troppe schermate uguali

    Se da A vai a B e da B torni ad A con un push, la pila cresce all'infinito. Usa pop per tornare indietro, oppure pushReplacement quando la schermata corrente non deve restare nello stack.

    5. Intercettare l'uscita per chiedere conferma

    Usa PopScope (che ha sostituito WillPopScope nelle versioni recenti di Flutter) per mostrare un dialog prima di abbandonare un form compilato a metà.

    Con questi mattoni puoi già strutturare la navigazione di app di piccola e media dimensione. Quando ti serviranno deep link, URL leggibili sul web o rotte annidate, il passo successivo naturale è go_router.

    class FormScreen extends StatelessWidget {
      const FormScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return PopScope(
          canPop: false,
          onPopInvokedWithResult: (didPop, result) async {
            if (didPop) return;
            final esci = await showDialog<bool>(
              context: context,
              builder: (context) => AlertDialog(
                title: const Text('Uscire senza salvare?'),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.pop(context, false),
                    child: const Text('Resta'),
                  ),
                  TextButton(
                    onPressed: () => Navigator.pop(context, true),
                    child: const Text('Esci'),
                  ),
                ],
              ),
            );
            if (esci == true && context.mounted) {
              Navigator.pop(context);
            }
          },
          child: Scaffold(
            appBar: AppBar(title: const Text('Nuovo prodotto')),
            body: const Center(child: Text('Form...')),
          ),
        );
      }
    }

    Risultato atteso

    Premendo il back sulla schermata del form compare un dialog di conferma; l'uscita avviene solo se l'utente conferma.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!