Flutter Navigation 2.0 e GoRouter nel 2026: Deep Linking e Domande da Colloquio

Padroneggiare la navigazione Flutter con GoRouter 17.5: routing dichiarativo, deep linking, ShellRoute, route guard e domande frequenti nei colloqui con esempi pratici.

Architettura di navigazione Flutter con diagramma di flusso deep linking GoRouter

La navigazione Flutter si è evoluta significativamente dall'introduzione di Navigation 2.0. GoRouter 17.5, il pacchetto ufficiale del team Flutter, gestisce la complessità della Router API offrendo routing dichiarativo, deep linking automatico e navigazione annidata di default.

GoRouter è feature-complete

Il team Flutter considera GoRouter feature-complete dal 2026. Supporta parametri di percorso, parametri di query, redirect, ShellRoute per UI persistente e StatefulShellRoute per preservare lo stato delle tab. Navigator 2.0 raw viene raramente implementato manualmente in produzione.

Comprendere l'architettura di Navigator 2.0

Navigator 2.0 ha introdotto un approccio dichiarativo e guidato da URL per la navigazione Flutter. Invece di chiamate imperative push e pop, lo stato dell'app determina cosa appare nello stack di navigazione. L'architettura si basa su tre classi principali:

  • Router: Il widget di livello superiore che coordina la navigazione
  • RouteInformationParser: Traduce gli URL in stato dell'app
  • RouterDelegate: Costruisce l'albero dei widget basandosi su quello stato

Quando un deep link arriva dal sistema operativo o un URL cambia nel browser web, il framework aggiorna lo stato e la UI reagisce. Il problema: scrivere un RouterDelegate e RouteInformationParser personalizzato a mano richiede un boilerplate significativo.

raw_navigator2_example.dartdart
// Raw Navigator 2.0 approach - verbose and rarely used directly
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
  
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  AppRoutePath? _currentPath;

  
  AppRoutePath? get currentConfiguration => _currentPath;

  
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: [
        const MaterialPage(child: HomeScreen()),
        if (_currentPath?.isProductPage == true)
          MaterialPage(child: ProductScreen(id: _currentPath!.productId!)),
      ],
      onDidRemovePage: (page) {
        // Handle page removal
      },
    );
  }

  
  Future<void> setNewRoutePath(AppRoutePath path) async {
    _currentPath = path;
    notifyListeners();
  }
}

Questa verbosità è il motivo per cui esiste GoRouter. Incapsula Navigator 2.0 e gestisce il boilerplate.

Configurare GoRouter 17.5

GoRouter 17.5 introduce il supporto per i metadati delle route e vincoli con espressioni regolari per i parametri di percorso. Il requisito minimo è Flutter 3.32 e Dart 3.8.

router_config.dartdart
import 'package:go_router/go_router.dart';

final goRouter = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true, // Useful during development
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'products/:id', // Path parameter
          name: 'product',
          builder: (context, state) {
            final productId = state.pathParameters['id']!;
            return ProductScreen(productId: productId);
          },
        ),
        GoRoute(
          path: 'search',
          name: 'search',
          builder: (context, state) {
            // Query parameters: /search?q=flutter&category=books
            final query = state.uri.queryParameters['q'] ?? '';
            final category = state.uri.queryParameters['category'];
            return SearchScreen(query: query, category: category);
          },
        ),
      ],
    ),
  ],
);

Il router si integra con MaterialApp.router:

main.dartdart
import 'package:flutter/material.dart';
import 'router_config.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: goRouter,
      title: 'GoRouter Demo',
    );
  }
}

Configurazione del Deep Linking per Android e iOS

Il deep linking permette agli URL esterni di aprire schermate specifiche nell'app. GoRouter gestisce il routing automaticamente una volta che la configurazione della piattaforma è in atto.

Android richiede un intent-filter in AndroidManifest.xml e un file Digital Asset Links ospitato sul dominio:

xml
<!-- android/app/src/main/AndroidManifest.xml -->
<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <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="example.com"
      android:pathPrefix="/products" />
  </intent-filter>
</activity>

Il file assetlinks.json deve essere servito all'indirizzo https://example.com/.well-known/assetlinks.json.

Per iOS, la capability Associated Domains deve essere abilitata in Xcode, e un file apple-app-site-association deve essere ospitato:

https://example.com/.well-known/apple-app-site-associationjson
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.app",
        "paths": ["/products/*", "/search"]
      }
    ]
  }
}

GoRouter 17.5 ha corretto un bug critico in cui i deep link Android con cold-start con percorsi vuoti perdevano schema e authority. Questo significa che i deep link ora funzionano in modo affidabile anche quando l'app non è in esecuzione.

Testare i deep link

Testare i deep link Android con adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123". Per iOS, usare xcrun simctl openurl booted "https://example.com/products/123".

ShellRoute per UI di navigazione persistente

ShellRoute avvolge le route figlie con un elemento UI persistente come una BottomNavigationBar o un Drawer. La shell rimane visibile durante la navigazione tra i suoi figli.

shell_router_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    ShellRoute(
      builder: (context, state, child) {
        return ScaffoldWithNavBar(child: child);
      },
      routes: [
        GoRoute(
          path: '/home',
          name: 'home',
          builder: (context, state) => const HomeTab(),
        ),
        GoRoute(
          path: '/explore',
          name: 'explore',
          builder: (context, state) => const ExploreTab(),
        ),
        GoRoute(
          path: '/profile',
          name: 'profile',
          builder: (context, state) => const ProfileTab(),
        ),
      ],
    ),
  ],
);

// scaffold_with_nav_bar.dart
class ScaffoldWithNavBar extends StatelessWidget {
  final Widget child;
  const ScaffoldWithNavBar({super.key, required this.child});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _calculateSelectedIndex(context),
        onTap: (index) => _onItemTapped(index, context),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }

  int _calculateSelectedIndex(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    if (location.startsWith('/explore')) return 1;
    if (location.startsWith('/profile')) return 2;
    return 0;
  }

  void _onItemTapped(int index, BuildContext context) {
    switch (index) {
      case 0: context.go('/home');
      case 1: context.go('/explore');
      case 2: context.go('/profile');
    }
  }
}

GoRouter 17.5 ha risolto un problema in cui i gesti di ritorno su iOS facevano il pop dell'intera ShellRoute invece che solo della sotto-route attiva.

Pronto a superare i tuoi colloqui su Flutter?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

StatefulShellRoute per preservare lo stato delle tab

Quando gli utenti cambiano tab, ShellRoute ricostruisce il figlio. StatefulShellRoute preserva lo stack di navigazione di ogni branch:

stateful_shell_config.dartdart
final goRouter = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeTab(),
              routes: [
                GoRoute(
                  path: 'details/:id',
                  builder: (context, state) => DetailsScreen(
                    id: state.pathParameters['id']!,
                  ),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/explore',
              builder: (context, state) => const ExploreTab(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/profile',
              builder: (context, state) => const ProfileTab(),
            ),
          ],
        ),
      ],
    ),
  ],
);

Con questa configurazione, se un utente naviga a /home/details/42, passa alla tab Explore, poi torna a Home, sarà ancora sulla schermata dei dettagli.

Route Guard con Redirect

I redirect gestiscono i controlli di autenticazione e la navigazione condizionale. Il callback redirect viene eseguito prima di ogni navigazione:

authenticated_router.dartdart
final goRouter = GoRouter(
  initialLocation: '/',
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final isLoggingIn = state.matchedLocation == '/login';

    // Redirect to login if not authenticated
    if (!isLoggedIn && !isLoggingIn) {
      return '/login?redirect=${state.uri}';
    }

    // Redirect away from login if already authenticated
    if (isLoggedIn && isLoggingIn) {
      final redirect = state.uri.queryParameters['redirect'];
      return redirect ?? '/';
    }

    return null; // No redirect
  },
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginScreen(),
    ),
    GoRoute(
      path: '/admin',
      redirect: (context, state) {
        // Route-level redirect for admin-only access
        if (!AuthService.instance.isAdmin) {
          return '/';
        }
        return null;
      },
      builder: (context, state) => const AdminScreen(),
    ),
  ],
);

GoRouter 17.5 ha aggiunto il supporto per l'uso di metodi di estensione del context come context.namedLocation() all'interno dei callback di redirect.

Domande frequenti sulla navigazione Flutter nei colloqui

Gli intervistatori testano frequentemente la conoscenza della navigazione perché tocca architettura, gestione dello stato e integrazione con la piattaforma. Ecco le domande che distinguono i candidati senior:

D: Quale problema risolve Navigator 2.0 che Navigator 1.0 non risolve?

Navigator 1.0 usa la navigazione imperativa: Navigator.push() e Navigator.pop(). L'app non può rappresentare lo stato di navigazione come URL, rendendo difficile il deep linking e il supporto web. Navigator 2.0 è dichiarativo: lo stato dell'app determina lo stack di navigazione, gli URL sono cittadini di prima classe, e i deep link funzionano automaticamente.

D: Quando si userebbe context.go() vs context.push()?

context.go('/path') sostituisce lo stack di navigazione corrente fino alla route corrispondente. context.push('/path') aggiunge una nuova route sopra lo stack esistente. Si usa go() per la navigazione di livello superiore (cambio tab, andare alla home) e push() per approfondire i dettagli preservando la navigazione indietro.

D: Come si preserva lo stato tra i cambi di tab?

Si usa StatefulShellRoute.indexedStack(). Mantiene stack di navigazione separati per ogni branch e li preserva quando si cambia tab. Senza di esso, cambiando tab si ricostruisce l'intero sotto-albero.

D: Come si passano oggetti complessi tra le route?

Esistono tre approcci. Primo, passare un ID nel parametro di percorso e recuperare l'oggetto nella schermata di destinazione. Secondo, usare il parametro extra di GoRouter per passare l'oggetto direttamente. Terzo, memorizzare l'oggetto in una soluzione di state management (Riverpod, Bloc) e accedervi dalla destinazione. Il primo approccio è migliore per il deep linking perché l'URL rimane condivisibile.

dart
// Passing via extra (not deep-linkable)
context.push('/product', extra: productObject);

// Receiving via extra
GoRoute(
  path: '/product',
  builder: (context, state) {
    final product = state.extra as Product?;
    return ProductScreen(product: product);
  },
),

D: Cos'è il callback onEnter in GoRouter 17.5?

Il callback onEnter viene eseguito quando si entra in una route e fornisce accesso sia allo stato della route corrente che a quella successiva. Permette di eseguire logica prima che la route carichi completamente, utile per analytics o pre-caricamento condizionale.

Route type-safe con generazione di codice

GoRouter supporta il routing type-safe attraverso la generazione di codice. Si definiscono classi di dati delle route e si lascia che build_runner generi il codice di navigazione:

routes.dartdart
import 'package:go_router/go_router.dart';

part 'routes.g.dart';

<HomeRoute>(path: '/')
class HomeRoute extends GoRouteData {
  const HomeRoute();

  
  Widget build(BuildContext context, GoRouterState state) => const HomeScreen();
}

<ProductRoute>(path: '/product/:id')
class ProductRoute extends GoRouteData {
  final String id;
  const ProductRoute({required this.id});

  
  Widget build(BuildContext context, GoRouterState state) =>
      ProductScreen(productId: id);
}

Dopo aver eseguito dart run build_runner build, si naviga con sicurezza a compile-time:

dart
// Type-safe navigation
ProductRoute(id: '123').go(context);

Questo elimina gli errori di battitura nei percorsi delle route e assicura che i parametri siano sempre forniti.

Padroneggiare la navigazione Flutter per app in produzione

  • GoRouter 17.5 è la soluzione di navigazione raccomandata per le app Flutter che richiedono deep linking, navigazione annidata o supporto web
  • Si usa ShellRoute per bottom navigation bar persistenti; si usa StatefulShellRoute per preservare lo stack di navigazione di ogni tab
  • Il deep linking specifico per piattaforma (Android App Links, iOS Universal Links) viene configurato separatamente da GoRouter, che gestisce la logica di routing
  • I guard di autenticazione vengono implementati con il callback redirect a livello di router o di singola route
  • Per gli scenari di colloquio, ci si concentra sulla spiegazione del modello dichiarativo: lo stato dell'app guida la UI, gli URL sono derivati dallo stato, e GoRouter gestisce la traduzione
  • Le route type-safe con generazione di codice eliminano gli errori a runtime da percorsi scritti male
  • Consultare la documentazione sulla navigazione Flutter e il changelog di GoRouter per le funzionalità specifiche della versione

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Sfida del giorno

Sapresti trovare il bug in Flutter?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 23 agosto 2026

Tag

#flutter
#navigation
#gorouter
#deep-linking
#mobile

Condividi

Articoli correlati