# Flutter Navigation 2.0 and GoRouter in 2026: Deep Linking and Interview Questions > Master Flutter navigation with GoRouter 17.5: declarative routing, deep linking, ShellRoute, route guards, and common interview questions with practical examples. - Published: 2026-08-23 - Updated: 2026-08-23 - Author: Anthony Fillion-Maillet - Tags: flutter, navigation, gorouter, deep-linking, mobile - Reading time: 9 min --- Flutter navigation has evolved significantly since the introduction of Navigation 2.0. GoRouter 17.5, the official Flutter team package, now handles the complexity of the Router API while providing declarative routing, automatic deep linking, and nested navigation out of the box. > **GoRouter is feature-complete** > > The Flutter team considers GoRouter feature-complete as of 2026. It supports path parameters, query parameters, redirects, ShellRoute for persistent UI, and StatefulShellRoute for preserving tab state. Raw Navigator 2.0 is rarely hand-rolled in production anymore. ## Understanding Navigator 2.0 Architecture Navigator 2.0 introduced a declarative, URL-driven approach to Flutter navigation. Instead of imperative `push` and `pop` calls, the app state determines what appears on the navigation stack. The architecture relies on three core classes: - **Router**: The top-level widget that coordinates navigation - **RouteInformationParser**: Translates URLs into app state - **RouterDelegate**: Builds the widget tree based on that state When a deep link arrives from the OS or a URL changes in a web browser, the framework updates the state, and the UI reacts. The problem: writing a custom `RouterDelegate` and `RouteInformationParser` by hand requires significant boilerplate. ```dart // raw_navigator2_example.dart // Raw Navigator 2.0 approach - verbose and rarely used directly class AppRouterDelegate extends RouterDelegate with ChangeNotifier, PopNavigatorRouterDelegateMixin { @override final GlobalKey navigatorKey = GlobalKey(); AppRoutePath? _currentPath; @override AppRoutePath? get currentConfiguration => _currentPath; @override 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 }, ); } @override Future setNewRoutePath(AppRoutePath path) async { _currentPath = path; notifyListeners(); } } ``` This verbosity is why GoRouter exists. It wraps Navigator 2.0 and handles the boilerplate. ## Setting Up GoRouter 17.5 GoRouter 17.5 introduces route metadata support and regular expression constraints for path parameters. The minimum SDK requirement is Flutter 3.32 and Dart 3.8. ```dart // router_config.dart 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); }, ), ], ), ], ); ``` The router integrates with `MaterialApp.router`: ```dart // main.dart import 'package:flutter/material.dart'; import 'router_config.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: goRouter, title: 'GoRouter Demo', ); } } ``` ## Deep Linking Configuration for Android and iOS Deep linking allows external URLs to open specific screens in the app. GoRouter handles the routing automatically once the platform configuration is in place. ### Android App Links Android requires an `intent-filter` in `AndroidManifest.xml` and a Digital Asset Links file hosted on the domain: ```xml ``` The `assetlinks.json` file must be served at `https://example.com/.well-known/assetlinks.json`. ### iOS Universal Links For iOS, the `Associated Domains` capability must be enabled in Xcode, and an `apple-app-site-association` file must be hosted: ```json // https://example.com/.well-known/apple-app-site-association { "applinks": { "apps": [], "details": [ { "appID": "TEAMID.com.example.app", "paths": ["/products/*", "/search"] } ] } } ``` GoRouter 17.5 fixed a critical bug where Android cold-start deep links with empty paths lost the scheme and authority. This means deep links now work reliably even when the app is not running. > **Testing deep links** > > Test Android deep links with `adb shell am start -a android.intent.action.VIEW -d "https://example.com/products/123"`. For iOS, use `xcrun simctl openurl booted "https://example.com/products/123"`. ## ShellRoute for Persistent Navigation UI ShellRoute wraps child routes with a persistent UI element like a `BottomNavigationBar` or `Drawer`. The shell stays visible while navigating between its children. ```dart // shell_router_config.dart 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}); @override 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 fixed an issue where iOS back gestures would pop the entire `ShellRoute` instead of just the active sub-route. ## StatefulShellRoute for Preserving Tab State When users switch tabs, `ShellRoute` rebuilds the child. `StatefulShellRoute` preserves the navigation stack of each branch: ```dart // stateful_shell_config.dart 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(), ), ], ), ], ), ], ); ``` With this setup, if a user navigates to `/home/details/42`, switches to the Explore tab, then returns to Home, they will still be on the details screen. ## Route Guards with Redirect Redirects handle authentication checks and conditional navigation. The `redirect` callback runs before each navigation: ```dart // authenticated_router.dart 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 added support for using context extension methods like `context.namedLocation()` within redirect callbacks. ## Common Flutter Navigation Interview Questions Interviewers frequently test navigation knowledge because it touches architecture, state management, and platform integration. Here are questions that distinguish senior candidates: **Q: What problem does Navigator 2.0 solve that Navigator 1.0 does not?** Navigator 1.0 uses imperative navigation: `Navigator.push()` and `Navigator.pop()`. The app cannot represent the navigation state as a URL, making deep linking and web support difficult. Navigator 2.0 is declarative: the app state determines the navigation stack, URLs are first-class citizens, and deep links work automatically. **Q: When would you use `context.go()` vs `context.push()`?** `context.go('/path')` replaces the current navigation stack up to the matched route. `context.push('/path')` adds a new route on top of the existing stack. Use `go()` for top-level navigation (switching tabs, going home) and `push()` for drilling into details while preserving back navigation. **Q: How do you preserve state across tab switches?** Use `StatefulShellRoute.indexedStack()`. It maintains separate navigation stacks for each branch and preserves them when switching tabs. Without it, switching tabs rebuilds the entire sub-tree. **Q: How do you pass complex objects between routes?** Three approaches exist. First, pass an ID in the path parameter and fetch the object on the destination screen. Second, use GoRouter's `extra` parameter to pass the object directly. Third, store the object in a state management solution (Riverpod, Bloc) and access it from the destination. The first approach is best for deep linking because the URL remains shareable. ```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: What is the `onEnter` callback in GoRouter 17.5?** The `onEnter` callback executes when entering a route and provides access to both the current and next route states. It allows running logic before the route fully loads, useful for analytics or conditional pre-loading. ## Type-Safe Routes with code generation GoRouter supports type-safe routing through code generation. Define route data classes and let `build_runner` generate the navigation code: ```dart // routes.dart import 'package:go_router/go_router.dart'; part 'routes.g.dart'; @TypedGoRoute(path: '/') class HomeRoute extends GoRouteData { const HomeRoute(); @override Widget build(BuildContext context, GoRouterState state) => const HomeScreen(); } @TypedGoRoute(path: '/product/:id') class ProductRoute extends GoRouteData { final String id; const ProductRoute({required this.id}); @override Widget build(BuildContext context, GoRouterState state) => ProductScreen(productId: id); } ``` After running `dart run build_runner build`, navigate with compile-time safety: ```dart // Type-safe navigation ProductRoute(id: '123').go(context); ``` This eliminates typos in route paths and ensures parameters are always provided. ## Mastering Flutter Navigation for Production Apps - GoRouter 17.5 is the recommended navigation solution for Flutter apps requiring deep linking, nested navigation, or web support - Use `ShellRoute` for persistent bottom navigation bars; use `StatefulShellRoute` to preserve each tab's navigation stack - Configure platform-specific deep linking (Android App Links, iOS Universal Links) separately from GoRouter, which handles the routing logic - Implement authentication guards with the `redirect` callback at the router level or individual route level - For interview scenarios, focus on explaining the declarative model: app state drives the UI, URLs are derived from state, and GoRouter handles the translation - Type-safe routes with code generation eliminate runtime errors from mistyped paths - Check the [Flutter navigation documentation](https://docs.flutter.dev/ui/navigation) and [GoRouter changelog](https://pub.dev/packages/go_router/changelog) for version-specific features --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/flutter/flutter-navigation-gorouter-deep-linking-2026