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 base | go_router |
---|---|---|
Routing dichiarativo | No | Sì |
Parametri nell’URL | No | Sì |
Deep linking Web | No | Sì |
Redirezione condizionata | No | Sì |
Login/logout flow semplificato | No | Sì |
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
.
Navigare verso una rotta con parametro
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')),
);
}
}
Navigazione nidificata con ShellRoute
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 autenticatoUnauthenticatedState
: 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(),
),
],
);
Navigazione e aggiornamento dello stato
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 attraversorefreshListenable
- 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
Navigazione condizionata dopo eventi asincroni
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
- Mostrare una schermata di caricamento (
SplashScreen
) durante la verifica - 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:
digo_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
Deep linking reale (da link esterni)
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:
- Apri
android/app/src/main/AndroidManifest.xml
- 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>
- Se usi Firebase Dynamic Links, configura anche i file richiesti da Firebase.
iOS: configurazione del file Info.plist
- Apri
ios/Runner/Info.plist
- Aggiungi uno schema URL personalizzato:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>tuo-schema</string>
</array>
</dict>
</array>
- Per i Universal Links, è necessario anche pubblicare un file
apple-app-site-association
sul dominio.
Test del deep link su /home/details/7
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
- Login e autenticazione simulata
- Schermata
/login
con pulsante “Accedi” - Dopo il login, redirect automatico a
/home
- Logout disponibile da
/home
- Home protetta da autenticazione
- Accessibile solo se autenticati (con
redirect
) - Visualizza una lista con link a
/home/dettagli/:id
- Dettaglio con parametro dinamico
- Schermata che mostra il parametro
id
passato da URL
- Navigazione nidificata con ShellRoute
- Layout con
BottomNavigationBar
tra/home/tab1
e/home/tab2
- 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
- Modello dati
- Crea una classe
ImageItem
con: String imageUrl
String title
String description
- 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
- Schermata di dettaglio (
/detail/:id
oppure/gallery/:id
)
- Mostra immagine grande, titolo e descrizione
- Eventualmente un bottone per tornare alla griglia
- Gestione rotta errata (
/xyz
o simili)
- Mostra schermata “Elemento non trovato”
Suggerimenti
- Popola l’elenco con una lista statica di
ImageItem
- Usa
GridView.builder
conSliverGridDelegateWithFixedCrossAxisCount
- 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:
- Avvolgi l’immagine nella griglia con:
Hero(
tag: item.imageUrl,
child: Image.network(item.imageUrl),
)
- Fai lo stesso nella schermata di dettaglio:
Hero(
tag: item.imageUrl,
child: Image.network(item.imageUrl),
)
- Assicurati che il
tag
sia univoco e identico in entrambe le schermate.