Flutter Navigation 2.0 et GoRouter en 2026 : Deep Linking et Questions d'Entretien

Maîtriser la navigation Flutter avec GoRouter 17.5 : routage déclaratif, deep linking, ShellRoute, guards de route et questions d'entretien avec exemples pratiques.

Diagramme de l'architecture de navigation Flutter avec flux de deep linking GoRouter

La navigation Flutter a considérablement évolué depuis l'introduction de Navigation 2.0. GoRouter 17.5, le package officiel de l'équipe Flutter, gère désormais la complexité de l'API Router tout en fournissant un routage déclaratif, le deep linking automatique et la navigation imbriquée prêts à l'emploi.

GoRouter est fonctionnellement complet

L'équipe Flutter considère GoRouter comme fonctionnellement complet depuis 2026. Il prend en charge les paramètres de chemin, les paramètres de requête, les redirections, ShellRoute pour une interface utilisateur persistante et StatefulShellRoute pour préserver l'état des onglets. Le Navigator 2.0 brut n'est plus guère écrit manuellement en production.

Comprendre l'Architecture de Navigator 2.0

Navigator 2.0 a introduit une approche déclarative et pilotée par les URL pour la navigation Flutter. Au lieu d'appels impératifs push et pop, l'état de l'application détermine ce qui apparaît dans la pile de navigation. L'architecture repose sur trois classes fondamentales :

  • Router : Le widget de niveau supérieur qui coordonne la navigation
  • RouteInformationParser : Traduit les URL en état d'application
  • RouterDelegate : Construit l'arbre de widgets en fonction de cet état

Lorsqu'un deep link arrive du système d'exploitation ou qu'une URL change dans un navigateur web, le framework met à jour l'état et l'interface réagit. Le problème : écrire un RouterDelegate et un RouteInformationParser personnalisés manuellement nécessite un code boilerplate conséquent.

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();
  }
}

Cette verbosité explique l'existence de GoRouter. Il encapsule Navigator 2.0 et gère le code boilerplate.

Configuration de GoRouter 17.5

GoRouter 17.5 introduit le support des métadonnées de route et les contraintes par expressions régulières pour les paramètres de chemin. Le SDK minimum requis est Flutter 3.32 et 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);
          },
        ),
      ],
    ),
  ],
);

Le router s'intègre avec 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',
    );
  }
}

Configuration du Deep Linking pour Android et iOS

Le deep linking permet aux URL externes d'ouvrir des écrans spécifiques dans l'application. GoRouter gère automatiquement le routage une fois la configuration de la plateforme en place.

Android nécessite un intent-filter dans AndroidManifest.xml et un fichier Digital Asset Links hébergé sur le domaine :

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>

Le fichier assetlinks.json doit être servi à l'adresse https://example.com/.well-known/assetlinks.json.

Pour iOS, la capacité Associated Domains doit être activée dans Xcode, et un fichier apple-app-site-association doit être hébergé :

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

GoRouter 17.5 a corrigé un bug critique où les deep links Android lors d'un démarrage à froid avec des chemins vides perdaient le schéma et l'autorité. Cela signifie que les deep links fonctionnent désormais de manière fiable même lorsque l'application n'est pas en cours d'exécution.

Tester les deep links

Pour tester les deep links Android, utiliser adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123". Pour iOS, utiliser xcrun simctl openurl booted "https://example.com/products/123".

ShellRoute pour une Interface de Navigation Persistante

ShellRoute enveloppe les routes enfants avec un élément d'interface utilisateur persistant comme une BottomNavigationBar ou un Drawer. Le shell reste visible lors de la navigation entre ses enfants.

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 a corrigé un problème où les gestes de retour iOS faisaient sortir l'intégralité du ShellRoute au lieu de la sous-route active uniquement.

Prêt à réussir tes entretiens Flutter ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

StatefulShellRoute pour Préserver l'État des Onglets

Lorsque les utilisateurs changent d'onglet, ShellRoute reconstruit l'enfant. StatefulShellRoute préserve la pile de navigation de chaque branche :

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(),
            ),
          ],
        ),
      ],
    ),
  ],
);

Avec cette configuration, si un utilisateur navigue vers /home/details/42, passe à l'onglet Explorer, puis revient à Accueil, il sera toujours sur l'écran de détails.

Guards de Route avec Redirect

Les redirections gèrent les vérifications d'authentification et la navigation conditionnelle. Le callback redirect s'exécute avant chaque navigation :

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 a ajouté le support de l'utilisation des méthodes d'extension de contexte comme context.namedLocation() dans les callbacks de redirection.

Questions d'Entretien Courantes sur la Navigation Flutter

Les recruteurs testent fréquemment les connaissances en navigation car elles touchent l'architecture, la gestion d'état et l'intégration de la plateforme. Voici des questions qui distinguent les candidats seniors :

Q : Quel problème Navigator 2.0 résout-il que Navigator 1.0 ne résout pas ?

Navigator 1.0 utilise la navigation impérative : Navigator.push() et Navigator.pop(). L'application ne peut pas représenter l'état de navigation comme une URL, rendant le deep linking et le support web difficiles. Navigator 2.0 est déclaratif : l'état de l'application détermine la pile de navigation, les URL sont des citoyens de première classe, et les deep links fonctionnent automatiquement.

Q : Quand utiliser context.go() vs context.push() ?

context.go('/path') remplace la pile de navigation actuelle jusqu'à la route correspondante. context.push('/path') ajoute une nouvelle route au-dessus de la pile existante. Utiliser go() pour la navigation de niveau supérieur (changement d'onglets, retour à l'accueil) et push() pour approfondir les détails tout en préservant la navigation arrière.

Q : Comment préserver l'état lors des changements d'onglets ?

Utiliser StatefulShellRoute.indexedStack(). Il maintient des piles de navigation séparées pour chaque branche et les préserve lors des changements d'onglets. Sans cela, changer d'onglet reconstruit l'intégralité du sous-arbre.

Q : Comment passer des objets complexes entre les routes ?

Trois approches existent. Premièrement, passer un ID dans le paramètre de chemin et récupérer l'objet sur l'écran de destination. Deuxièmement, utiliser le paramètre extra de GoRouter pour passer l'objet directement. Troisièmement, stocker l'objet dans une solution de gestion d'état (Riverpod, Bloc) et y accéder depuis la destination. La première approche est la meilleure pour le deep linking car l'URL reste partageable.

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);
  },
),

Q : Qu'est-ce que le callback onEnter dans GoRouter 17.5 ?

Le callback onEnter s'exécute lors de l'entrée dans une route et donne accès à la fois aux états de route actuel et suivant. Il permet d'exécuter de la logique avant que la route soit complètement chargée, utile pour l'analytics ou le pré-chargement conditionnel.

Routes Type-Safe avec Génération de Code

GoRouter prend en charge le routage type-safe grâce à la génération de code. Il suffit de définir des classes de données de route et de laisser build_runner générer le code de navigation :

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);
}

Après avoir exécuté dart run build_runner build, la navigation se fait avec une sécurité à la compilation :

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

Cela élimine les fautes de frappe dans les chemins de route et garantit que les paramètres sont toujours fournis.

Maîtriser la Navigation Flutter pour les Applications en Production

  • GoRouter 17.5 est la solution de navigation recommandée pour les applications Flutter nécessitant le deep linking, la navigation imbriquée ou le support web
  • Utiliser ShellRoute pour les barres de navigation inférieures persistantes ; utiliser StatefulShellRoute pour préserver la pile de navigation de chaque onglet
  • Configurer le deep linking spécifique à la plateforme (Android App Links, iOS Universal Links) séparément de GoRouter, qui gère la logique de routage
  • Implémenter les guards d'authentification avec le callback redirect au niveau du router ou au niveau de chaque route
  • Pour les scénarios d'entretien, se concentrer sur l'explication du modèle déclaratif : l'état de l'application pilote l'interface, les URL sont dérivées de l'état, et GoRouter gère la traduction
  • Les routes type-safe avec génération de code éliminent les erreurs d'exécution dues aux chemins mal typés
  • Consulter la documentation de navigation Flutter et le changelog de GoRouter pour les fonctionnalités spécifiques à chaque version

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Défi du jour

Tu saurais repérer le bug en Flutter ?

Un vrai bout de code, un bug caché, une tentative par jour. Sans compte pour essayer.

Anthony Fillion-Maillet

Écrit par

Anthony Fillion-Maillet

Fondateur de SharpSkill

Développeur fullstack depuis plus de 10 ans. Il dirige SharpSkill et répond de tout ce qui y est publié.

Mis à jour le 23 août 2026

Tags

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

Partager

Articles similaires