Flutter: navigazione e rotte

Anche in una semplice applicazione Flutter la gestione della navigazione tra le varie pagine è uno dei primi problemi che occorre spesso affrontare. Ad esempio, in uno dei post precedenti per mostrare differenti tipi di widget ho usato una pagina principale con una serie di pulsanti che rimandavano alle singole pagine.  Nel post di oggi vedremo due semplici approcci a questo problema.


Il metodo imperativo

In Flutter 1.0 la navigazione tra pagine utilizza sostanzialmente una struttura LIFO (Last In First Out) di widget in cui ogni route (pagina) può essere collocata in cima o rimossa dalla cima utilizzando la classe Navigator e specificamente i suoi metodi .push e .pop

Per un esempio, prepariamo innanzitutto un nuovo progetto Flutter che preveda almeno due rotte, la prima con un pulsante che rimanda alla seconda e la seconda con un pulsante che permette di tornare indietro alla prima pagina.


import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: 'Test navigazione',
    home: Main(),
  ));
}

class Main extends StatelessWidget {
  const Main({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Prima pagina'),
      ),
      body: Center(
        child: MaterialButton(
          color: Colors.red,
          child: const Text('Pagina 2'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const SecondPage()),
            );
          },
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  const SecondPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Seconda pagina"),
      ),
      body: Center(
        child: MaterialButton(
          color: Colors.amber,
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('Indietro!'),
        ),
      ),
    );
  }
}


Il metodo .push (che richiede due argomenti, il BuildContext e un PageBuilder), è utilizzato per mettere in cima allo stack la seconda pagina, mentre il metodo .pop (che richiede come unico argomento il BuildContext) è utilizzato per rimuovere l'ultimo elemento inserito nel stack (la seconda pagina) e quindi tornare alla prima.


Le rotte utilizzate nell'esempio sono rotte anonime, ma è possibile anche assegnare dei nomi alle rotte, anziché le classi, come nell'esempio successivo:


  runApp(MaterialApp(
    title: 'Test navigazione',
    home: Main(),
    routes: {
        "seconda": (context) => const SecondPage()
    },
  ));


e usare il metodo .pushNamed al posto di .push (il metodo .pop rimane lo stesso)


        child: MaterialButton(
          color: Colors.red,
          child: const Text('Pagina 2'),
          onPressed: () {
            Navigator.pushNamed(context, "seconda");
          },
        ),


Il metodo dichiarativo

Con Flutter 2.0 la navigazione tra pagine è diventata una funzione associata allo stato: il cambio di pagina può quindi essere gestito modificando lo stato dell'applicazione. Se proviamo a riscrivere il codice precedente usando un widget StatefulWidget ed aggiungendo anche una terza pagina, otteniamo


import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: 'Test navigazione',
    home: Main(),
  ));
}

class Main  extends StatefulWidget {
  const Main({Key? key}) : super(key: key);

  @override
  State<Main> createState() => _MainState();
}

class _MainState extends State<Main> {
  String _selected="prima";

  Page _buildMain(){
    return MaterialPage(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Prima pagina'),
        ),
        body: Center(
          child: Column(
            children: [
              MaterialButton(
                color: Colors.red,
                child: const Text('Pagina 2'),
                onPressed: (){
                  setState(() {
                    _selected="seconda";
                  });
                },
              ),
              MaterialButton(
                color: Colors.red,
                child: const Text('Pagina 3'),
                onPressed: (){
                  setState(() {
                    _selected="terza";
                  });
                },
              ),
            ],
          ),
        ),        
      ),
      key: const ValueKey("prima")
    );
  }

  Page _buildSecondPage(){
    return MaterialPage(
      child: Scaffold(
        appBar: AppBar(
          title: const Text("Seconda pagina"),
        ),
        body: Center(
        child: Column(
          children: [
            MaterialButton(
              color: Colors.amber,
              onPressed: (){
                setState(() {
                  _selected="prima";
                  }
                );
              },
              child: const Text('Pagina 1'),
             ),
            MaterialButton(
              color: Colors.amber,
              onPressed: (){
                setState(() {
                  _selected="terza";
                  }
                );
              },
              child: const Text('Pagina 3'),
             ),

          ],
        ),          
        ),
      ),
      key: const ValueKey("seconda")
    );
  }

  Page _buildThirdPage(){
    return MaterialPage(
      child: Scaffold(
        appBar: AppBar(
          title: const Text("Terza pagina"),
        ),
        body: Center(
        child: Column(
          children: [
            MaterialButton(
              color: Colors.pink,
              onPressed: (){
                setState(() {
                  _selected="prima";
                  }
                );
              },
              child: const Text('Pagina 1'),
             ),
            MaterialButton(
              color: Colors.pink,
              onPressed: (){
                setState(() {
                  _selected="seconda";
                  }
                );
              },
              child: const Text('Pagina 2'),
             ),

          ],
        ),          
        ),
      ),
      key: const ValueKey("terza")
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Navigator(
        onPopPage: (route, result) => route.didPop(result),
        pages: [
           _buildMain(),
          if (_selected=="seconda") _buildSecondPage(),
          if (_selected=="terza") _buildThirdPage(),
        ],
      ),
    );
  }
}


Con questo tipo di scrittura il cambio pagina si ottiene perché modificando la variabile  _selected
 si scatena un cambio di stato e il metodo build si occupa di visualizzare la pagina selezionata. 

La navigazione non avviene più in modo lineare (in avanti con il push e indietro con il pop), ma può avvenire in qualsiasi ordine.

Da notare come con  String _selected="prima"; impostiamo la rotta iniziale.
 
Sicuramente nei prossimi articoli torneremo sulla navigazione e sulle rotte, introducendo altri elementi  necessari per riuscire a gestire situazioni più complesse.

Come per gli articoli precedenti potete trovare il codice completo tra le mie repo GitHub:

Buon lavoro !


Commenti