Skip to Content
FlutterLezione 4 Navigazione4.4 - Navigazione con go_router

Lezione 4.2 - Navigazione con go_router

I limiti della navigazione base con Navigator

Il sistema di navigazione base in Flutter utilizza la classe Navigator e metodi imperativi come push() e pop(). Sebbene sia adatto per app semplici, presenta diversi limiti man mano che l’applicazione cresce in complessità.

1. Navigazione imperativa e verbosa

Ogni passaggio di schermata richiede chiamate esplicite:

Navigator.push( context, MaterialPageRoute(builder: (context) => DettaglioPagina(id: 42)), );

Questo approccio è ripetitivo e rende difficile mantenere una panoramica delle rotte in un’app grande.


2. Difficoltà nel gestire URL e deep linking (soprattutto su Web)

Flutter Web supporta URL nel browser, ma con il sistema base bisogna gestire manualmente:

  • la generazione e il parsing degli URL
  • la sincronizzazione tra stato dell’app e URL

Il Navigator base non è pensato per gestire in modo naturale i deep link o la condivisione di link.


3. Routing non dichiarativo

Le rotte non sono centralizzate o visibili a colpo d’occhio, rendendo difficile:

  • sapere quali schermate sono disponibili
  • validare parametri o eseguire redirect automatici

4. Gestione complessa dei parametri

Per passare dati tra schermate bisogna usare costruttori e spesso si perde il collegamento con l’URL:

Navigator.push(context, MaterialPageRoute( builder: (context) => DettaglioPage(todoId: 7), ));

Non esiste un mapping diretto tra URL (/todo/7) e dati passati nel costruttore.


5. Login flow e redirect

Non esiste un sistema integrato per:

  • bloccare l’accesso a certe rotte se non autenticati
  • eseguire redirect automatici in base allo stato utente (es. /login/home dopo login)

Serve implementare tutto manualmente.


Introduzione a go_router come soluzione moderna

go_router è una libreria ufficialmente supportata dal team Flutter che rivoluziona la gestione della navigazione, introducendo un approccio dichiarativo, scalabile e adatto al web.

Cos’è go_router

go_router è un package che fornisce un sistema di routing centralizzato e dichiarativo. A differenza del Navigator, le rotte vengono definite in un’unica configurazione iniziale.

È progettato per semplificare flussi complessi come login, deep linking e navigazione condizionata.


Perché usare go_router

  • Tutte le rotte sono definite in un’unica struttura leggibile
  • Supporta parametri dinamici, query, redirect e named routes
  • Gestisce nativamente deep link (Web e Mobile)
  • Si integra facilmente con BLoC, Provider o altre soluzioni di stato

Quando è utile

  • App multipagina con molte schermate
  • App web che devono riflettere l’URL nel browser
  • App con autenticazione (login/logout) o flussi protetti

Confronto rapido

FunzionalitàNavigator basego_router
Routing dichiarativoNo
Parametri nell’URLNo
Deep linking WebNo
Redirezione condizionataNo
Login/logout flow semplificatoNo

Configurare go_router nel progetto Flutter

Per iniziare a usare go_router in un progetto Flutter, è necessario seguire alcuni passaggi fondamentali. Questa configurazione ti permetterà di definire tutte le rotte della tua applicazione in modo centralizzato e dichiarativo.

1. Aggiungere la dipendenza nel file pubspec.yaml

dependencies: flutter: sdk: flutter go_router: ^13.0.0

Dopo aver aggiunto la dipendenza, esegui:

flutter pub get

2. Definire le rotte nel main.dart

Importa i pacchetti necessari:

import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart';

Crea una configurazione delle rotte con GoRouter:

final GoRouter _router = GoRouter( initialLocation: '/login', routes: [ GoRoute( path: '/login', builder: (context, state) => LoginPage(), ), GoRoute( path: '/home', builder: (context, state) => HomePage(), ), ], );

3. Usare MaterialApp.router

Nel metodo build dell’app principale, sostituisci MaterialApp con MaterialApp.router:

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, title: 'Esempio GoRouter', debugShowCheckedModeBanner: false, ); } }

4. Navigare tra le pagine

Per spostarti da una schermata all’altra, usa:

context.go('/home');

oppure, se vuoi aggiungere alla cronologia:

context.push('/home');

Gestire parametri dinamici e redirect

go_router permette di passare parametri dinamici direttamente nell’URL e di gestire i redirect in modo dichiarativo. Questo rende semplice creare percorsi come /user/42 o proteggere rotte tramite condizioni.

Parametri dinamici nell’URL

Per definire un parametro dinamico, si usa :nomeParametro nel percorso:

GoRoute( path: '/user/:id', builder: (context, state) { final userId = state.pathParameters['id']; return UserPage(userId: userId); }, )

In questo esempio, la rotta /user/42 passerà 42 come parametro id alla schermata UserPage.

Puoi costruire il percorso dinamico concatenando il valore del parametro:

context.go('/user/42');

oppure:

final id = '123'; context.push('/user/$id');

Redirect automatici

go_router consente di specificare un redirect globale (o per singola rotta) per reindirizzare l’utente in base a una condizione:

final GoRouter _router = GoRouter( redirect: (context, state) { final loggedIn = false; // da Provider o Bloc final accessingLogin = state.uri.path == '/login'; if (!loggedIn && !accessingLogin) { return '/login'; } if (loggedIn && accessingLogin) { return '/home'; } return null; }, routes: [...], );

Redirect specifico per una rotta

GoRoute( path: '/admin', redirect: (context, state) => isAdmin ? null : '/unauthorized', builder: (context, state) => AdminPage(), )

Considerazioni

  • I parametri dinamici sono utili per dettagli, profili, elementi da identificare univocamente
  • I redirect permettono di creare flussi di login sicuri e intuitivi

Esercizio guidato - Realizzare una mini app con 3 schermate

In questa sezione realizziamo una semplice applicazione Flutter che utilizza go_router per gestire tre schermate:

  • /login
  • /home
  • /home/details/:id

L’obiettivo è mostrare in pratica come si configurano le rotte, si naviga tra le schermate e si utilizzano i parametri dinamici.

1. Definizione delle rotte con GoRouter

final GoRouter _router = GoRouter( initialLocation: '/login', routes: [ GoRoute( path: '/login', builder: (context, state) => LoginPage(), ), GoRoute( path: '/home', builder: (context, state) => HomePage(), routes: [ GoRoute( path: 'details/:id', builder: (context, state) { final id = state.pathParameters['id']!; return DetailsPage(id: id); }, ), ], ), ], );

2. Componente LoginPage

class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Login')), body: Center( child: ElevatedButton( onPressed: () => context.go('/home'), child: Text('Accedi'), ), ), ); } }

3. Componente HomePage

class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home')), body: ListView.builder( itemCount: 5, itemBuilder: (context, index) { return ListTile( title: Text('Elemento #$index'), onTap: () => context.go('/home/details/$index'), ); }, ), ); } }

4. Componente DetailsPage

class DetailsPage extends StatelessWidget { final String id; const DetailsPage({required this.id}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Dettaglio')), body: Center(child: Text('Hai selezionato l'elemento #$id')), ); } }

Quando un’app richiede un layout condiviso tra più pagine (ad esempio una barra inferiore di navigazione o un menu laterale), go_router permette di definire una struttura di routing nidificata utilizzando ShellRoute.

Con ShellRoute, è possibile:

  • Mostrare un layout comune tra più schermate
  • Mantenere lo stato tra le rotte figlie
  • Creare strutture a tab persistenti

Esempio: layout con barra inferiore (bottom navigation)

Definiamo tre schermate figlie all’interno di un layout condiviso:

final GoRouter _router = GoRouter( initialLocation: '/home/tab1', routes: [ GoRoute( path: '/login', builder: (context, state) => LoginPage(), ), ShellRoute( builder: (context, state, child) { return Scaffold( appBar: AppBar(title: Text('App')), body: child, bottomNavigationBar: BottomNavigationBar( currentIndex: _calculateIndex(state.location), onTap: (index) { switch (index) { case 0: context.go('/home/tab1'); break; case 1: context.go('/home/tab2'); break; } }, items: [ BottomNavigationBarItem(icon: Icon(Icons.list), label: 'Tab 1'), BottomNavigationBarItem(icon: Icon(Icons.info), label: 'Tab 2'), ], ), ); }, routes: [ GoRoute( path: '/home/tab1', builder: (context, state) => Tab1Page(), ), GoRoute( path: '/home/tab2', builder: (context, state) => Tab2Page(), ), ], ), ], ); int _calculateIndex(String location) { if (location.startsWith('/home/tab2')) return 1; return 0; }

Considerazioni

  • Il layout comune viene renderizzato una volta sola e i figli cambiano all’interno di esso
  • Lo stato della BottomNavigationBar viene mantenuto navigando tra le schermate
  • Si possono anche usare altri widget di layout condivisi come Drawer, NavigationRail o layout a colonne

Gestione dello stato di autenticazione

Molte applicazioni Flutter necessitano di proteggere alcune schermate in base allo stato dell’utente, ad esempio consentendo l’accesso solo dopo il login. Con go_router, questa logica può essere implementata in modo dichiarativo tramite un meccanismo chiamato redirect, che consente di definire condizioni per reindirizzare l’utente verso una rotta specifica in base al suo stato.

Per controllare lo stato dell’autenticazione, possiamo utilizzare il pattern Bloc, che ci permette di gestire in modo centralizzato e reattivo lo stato utente.

Creazione di un AuthBloc

Definiamo un bloc che rappresenta lo stato di autenticazione. Questo bloc avrà due stati principali:

  • AuthenticatedState: utente autenticato
  • UnauthenticatedState: utente non autenticato

Eventi

abstract class AuthEvent {} class LoginEvent extends AuthEvent {} class LogoutEvent extends AuthEvent {}

Stati

abstract class AuthState {} class AuthenticatedState extends AuthState {} class UnauthenticatedState extends AuthState {}

Bloc

class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc() : super(UnauthenticatedState()) { on<LoginEvent>((event, emit) => emit(AuthenticatedState())); on<LogoutEvent>((event, emit) => emit(UnauthenticatedState())); } }

Integrazione con go_router

go_router non è automaticamente connesso al sistema di stato (come Bloc). Per renderlo reattivo ai cambiamenti, bisogna fornire un oggetto che implementi Listenable, così da notificare go_router quando lo stato cambia. Creiamo quindi un ChangeNotifier che ascolta lo stream del bloc:

class BlocRouterListener extends ChangeNotifier { BlocRouterListener(Stream stream) { stream.listen((_) => notifyListeners()); } }

Configurazione del router

Costruiamo il router utilizzando redirect per gestire gli accessi e refreshListenable per permettere a go_router di reagire ai cambiamenti nello stato:

final GoRouter router = GoRouter( refreshListenable: BlocRouterListener(authBloc.stream), redirect: (context, state) { final isLoggedIn = authBloc.state is AuthenticatedState; final isGoingToLogin = state.uri.path == '/login'; if (!isLoggedIn && !isGoingToLogin) { return '/login'; } if (isLoggedIn && isGoingToLogin) { return '/home'; } return null; }, routes: [ GoRoute( path: '/login', builder: (context, state) => LoginPage(), ), GoRoute( path: '/home', builder: (context, state) => HomePage(), ), ], );

Quando l’utente effettua l’accesso, inviamo l’evento al bloc. Se il redirect è configurato correttamente, l’utente verrà automaticamente reindirizzato:

ElevatedButton( onPressed: () { context.read<AuthBloc>().add(LoginEvent()); }, child: Text('Accedi'), );

Logout:

IconButton( onPressed: () { context.read<AuthBloc>().add(LogoutEvent()); }, icon: Icon(Icons.logout), );

Considerazioni

  • Il bloc gestisce l’autenticazione in modo chiaro e isolato dal routing
  • go_router reagisce ai cambiamenti di stato attraverso refreshListenable
  • La logica di accesso è centralizzata e facilmente scalabile

Query parameters e fragment

Con go_router è possibile accedere ai query parameters e ai frammenti (fragment) dell’URL in modo semplice tramite l’oggetto GoRouterState. Questo è particolarmente utile per implementare filtri, paginazione, ricerca e ancoraggi su una pagina.

Differenza tra path parameters e query parameters

  • Path parameters: fanno parte del percorso e vengono dichiarati con i due punti :

  • Esempio: /user/:id

  • Si recuperano con state.pathParameters['id']

  • Query parameters: vengono aggiunti alla fine del percorso con ?

  • Esempio: /search?term=flutter&page=2

  • Si recuperano con state.uri.queryParameters['term']

Recuperare i query parameters

Se abbiamo una rotta come:

GoRoute( path: '/search', builder: (context, state) { final term = state.uri.queryParameters['term'] ?? ''; final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1; return SearchPage(term: term, page: page); }, )

Navigare verso questa rotta con parametri:

context.go('/search?term=flutter&page=2');

Uso del fragment (ancora)

Il frammento (parte dopo #) può essere letto con:

final anchor = state.uri.fragment;

Esempio di URL: /terms#privacy

Vantaggi dell’approccio con queryParameters

  • Non richiede modifiche al path
  • Si presta bene per UI dinamiche (es. tab, filtri multipli)

Considerazioni

  • I parametri query sono opzionali e non causano errore se assenti
  • Sono stringhe per default: vanno convertiti manualmente se servono numeri o booleani
  • Possono essere generati dinamicamente per condividere link

In alcune situazioni è necessario attendere il completamento di operazioni asincrone prima di consentire la navigazione o decidere verso quale rotta reindirizzare l’utente. Questo accade, ad esempio, durante la fase di inizializzazione dell’applicazione o dopo il login, quando bisogna validare un token memorizzato.

Caso d’uso: verifica asincrona del token

Immaginiamo che all’avvio dell’app si debba verificare se l’utente ha un token JWT valido salvato nei SharedPreferences. Questa operazione è asincrona e richiede un Future.

Strategia

  1. Mostrare una schermata di caricamento (SplashScreen) durante la verifica
  2. Dopo il completamento della verifica, navigare a /home o /login in base al risultato

Esempio

class SplashScreen extends StatefulWidget { @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { @override void initState() { super.initState(); _checkAuth(); } Future<void> _checkAuth() async { final prefs = await SharedPreferences.getInstance(); final token = prefs.getString('token'); // Simula chiamata API per validazione token final isValid = await AuthService.validateToken(token); if (isValid) { context.go('/home'); } else { context.go('/login'); } } @override Widget build(BuildContext context) { return Scaffold( body: Center(child: CircularProgressIndicator()), ); } }

Alternativa: redirect asincrono nel bloc di autenticazione

Se utilizzi Bloc, puoi lanciare la verifica del token in fase di inizializzazione:

class AuthBloc extends Bloc<AuthEvent, AuthState> { AuthBloc() : super(LoadingAuthState()) { on<CheckTokenEvent>((event, emit) async { final token = await _loadToken(); final valid = await AuthService.validateToken(token); emit(valid ? AuthenticatedState() : UnauthenticatedState()); }); } }

E impostare lo stato iniziale della tua app su una schermata di caricamento finché il bloc non cambia stato.

Considerazioni

  • È importante non forzare redirect asincroni direttamente nel redirect: di go_router, poiché esso deve essere sincrono
  • Tutta la logica asincrona dovrebbe essere gestita prima della costruzione del router oppure tramite uno stato intermedio visibile all’utente (es. splash screen)
  • Questo pattern è fondamentale per flussi di autenticazione, onboarding o caricamento dati iniziali

Il deep linking consente all’utente di aprire una schermata specifica della tua app Flutter direttamente da un link esterno. Ad esempio, cliccando su un link ricevuto via email o da una pagina web, la tua app può aprirsi e mostrare subito il contenuto corrispondente. Con go_router, questo processo è supportato nativamente e funziona su tutte le principali piattaforme: Web, Android e iOS.

Web: nessuna configurazione aggiuntiva

Su Flutter Web, il supporto al deep linking è automatico. Quando un utente apre un URL completo nel browser, go_router riconosce il percorso e carica la schermata corrispondente.

Esempio:

https://tuo-sito.web.app/home/details/7

Apre direttamente la schermata /home/details/7 se la rotta è correttamente definita nel GoRouter.

Requisiti:

  • Usa MaterialApp.router
  • Usa routerConfig
  • Evita HashUrlStrategy() se non necessaria

Android: configurazione dell’intent filter

Per abilitare il deep linking su Android:

  1. Apri android/app/src/main/AndroidManifest.xml
  2. Aggiungi questo blocco dentro l’activity:
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="tuo-dominio.com" /> </intent-filter>
  1. Se usi Firebase Dynamic Links, configura anche i file richiesti da Firebase.

iOS: configurazione del file Info.plist

  1. Apri ios/Runner/Info.plist
  2. Aggiungi uno schema URL personalizzato:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>tuo-schema</string> </array> </dict> </array>
  1. Per i Universal Links, è necessario anche pubblicare un file apple-app-site-association sul dominio.

Web

Apri direttamente nel browser:

http://localhost:5000/home/details/7

Android (via terminale con adb)

adb shell am start -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "https://tuo-dominio.com/home/details/7"

iOS

Apri Safari e inserisci l’URL:

https://tuo-dominio.com/home/details/7

Rotta da definire in GoRouter

Assicurati che nel tuo router ci sia:

GoRoute( path: '/home/details/:id', builder: (context, state) { final id = state.pathParameters['id']; return DetailsPage(id: id); }, )

Considerazioni

  • Il deep linking consente l’apertura diretta di schermate specifiche via URL
  • Richiede configurazione lato sistema operativo su Android e iOS
  • È molto utile per notifiche push, campagne marketing, condivisioni tra utenti o navigazione avanzata da browser

Fallback per rotte non esistenti

In qualsiasi applicazione, può capitare che l’utente inserisca manualmente un URL errato o riceva un link non valido. In questi casi, è importante fornire una schermata di fallback (spesso chiamata “pagina 404”) per comunicare in modo chiaro che la rotta richiesta non esiste.

Con go_router, puoi gestire questi casi definendo una proprietà errorBuilder nel costruttore del router.

Esempio di gestione fallback

final GoRouter router = GoRouter( routes: [ GoRoute( path: '/home', builder: (context, state) => HomePage(), ), // altre rotte... ], errorBuilder: (context, state) => NotFoundPage(), );

Schermata personalizzata per 404

class NotFoundPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Pagina non trovata')), body: Center( child: Text( 'La pagina che stai cercando non esiste.', style: TextStyle(fontSize: 18), ), ), ); } }

Quando viene mostrata questa schermata?

  • Quando un utente apre un URL che non corrisponde a nessuna rotta definita
  • Quando si tenta di navigare verso una rotta malformata o mancante

Esercizio live - Dimostrazione go_router

Obiettivo: realizzare una mini app che dimostri i concetti fondamentali di go_router, tra cui:

  • Navigazione dichiarativa
  • Parametri dinamici
  • Redirect condizionati
  • Navigazione nidificata (ShellRoute)
  • Schermata 404 personalizzata

Funzionalità da realizzare

  1. Login e autenticazione simulata
  • Schermata /login con pulsante “Accedi”
  • Dopo il login, redirect automatico a /home
  • Logout disponibile da /home
  1. Home protetta da autenticazione
  • Accessibile solo se autenticati (con redirect)
  • Visualizza una lista con link a /home/dettagli/:id
  1. Dettaglio con parametro dinamico
  • Schermata che mostra il parametro id passato da URL
  1. Navigazione nidificata con ShellRoute
  • Layout con BottomNavigationBar tra /home/tab1 e /home/tab2
  1. Fallback 404
  • Se si digita una rotta errata, mostra pagina “Non trovata” personalizzata

Struttura delle rotte da implementare

/ /login /home /home/tab1 /home/tab2 /home/dettagli/:id /xyz → NotFoundPage

Suggerimenti tecnici

  • Simula lo stato di autenticazione con un ChangeNotifier o Bloc molto semplice
  • Usa refreshListenable per aggiornare il router in base allo stato auth
  • Per ShellRoute, mantieni un layout fisso con barra inferiore
  • Implementa errorBuilder per la pagina 404

Esercizio - GridView con dettagli

Obiettivo: realizzare una mini app Flutter che mostri una griglia di immagini e, al click su ciascuna, apra una pagina con ulteriori dettagli. L’app aiuta a consolidare:

  • Uso di GridView con dati dinamici
  • Navigazione con go_router
  • Passaggio di oggetti o parametri
  • Separazione tra UI e modello dati

Requisiti

  1. Modello dati
  • Crea una classe ImageItem con:
  • String imageUrl
  • String title
  • String description
  1. Griglia principale (/gallery)
  • Mostra almeno 6 elementi in una griglia
  • Ogni cella mostra l’immagine e il titolo
  • Al tap, naviga a /gallery/:id oppure /detail?id=123 con i dati del modello
  1. Schermata di dettaglio (/detail/:id oppure /gallery/:id)
  • Mostra immagine grande, titolo e descrizione
  • Eventualmente un bottone per tornare alla griglia
  1. Gestione rotta errata (/xyz o simili)
  • Mostra schermata “Elemento non trovato”

Suggerimenti

  • Popola l’elenco con una lista statica di ImageItem
  • Usa GridView.builder con SliverGridDelegateWithFixedCrossAxisCount
  • Per passare i dati, puoi:
  • usare state.extra
  • oppure un ID e recuperare il modello da una lista in memoria

Struttura suggerita

/lib /models image_item.dart /screens gallery_screen.dart detail_screen.dart not_found_screen.dart /router app_router.dart main.dart

Hero Animation (Bonus avanzato)

Per rendere la transizione più fluida tra la griglia e la schermata di dettaglio, puoi usare il widget Hero di Flutter:

  1. Avvolgi l’immagine nella griglia con:
Hero( tag: item.imageUrl, child: Image.network(item.imageUrl), )
  1. Fai lo stesso nella schermata di dettaglio:
Hero( tag: item.imageUrl, child: Image.network(item.imageUrl), )
  1. Assicurati che il tag sia univoco e identico in entrambe le schermate.