iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Navigation / go_router

Flutter navigation moves between screens (routes). The classic Navigator push/pop API is fine for small apps; go_router is the recommended approach for production: declarative routes, deep linking, type-safe params, nested shells.

Navigator, go_router, params, deep links

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

// 1) Imperative Navigator API
class HomePage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: const Text('Home')),
            body: Center(
                child: ElevatedButton(
                    onPressed: () {
                        Navigator.of(context).push(MaterialPageRoute(
                            builder: (_) => const DetailsPage(productId: 42),
                        ));
                    },
                    child: const Text('Open details'),
                ),
            ),
        );
    }
}

class DetailsPage extends StatelessWidget {
    final int productId;
    const DetailsPage({super.key, required this.productId});

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text('Product $productId')),
            body: Center(
                child: ElevatedButton(
                    onPressed: () => Navigator.of(context).pop('purchased'),
                    child: const Text('Buy'),
                ),
            ),
        );
    }
}

// 2) Receive the popped result
final result = await Navigator.of(context).push<String>(MaterialPageRoute(
    builder: (_) => const DetailsPage(productId: 42),
));
if (result == 'purchased') showSnack(context, 'Thanks for buying!');

// 3) Named routes (small apps only)
MaterialApp(
    initialRoute: '/',
    routes: {
        '/':        (_) => const HomePage(),
        '/details': (ctx) {
            final args = ModalRoute.of(ctx)!.settings.arguments as Map;
            return DetailsPage(productId: args['productId']);
        },
    },
);
Navigator.pushNamed(context, '/details', arguments: {'productId': 42});

// 4) go_router — the modern choice
// pubspec.yaml: go_router: ^14.0.0
final _router = GoRouter(
    initialLocation: '/',
    routes: [
        GoRoute(path: '/', builder: (_, __) => const HomePage()),
        GoRoute(
            path: '/products/:id',
            builder: (ctx, state) {
                final id = int.parse(state.pathParameters['id']!);
                return DetailsPage(productId: id);
            },
            routes: [
                GoRoute(
                    path: 'reviews',
                    builder: (_, state) {
                        final id = int.parse(state.pathParameters['id']!);
                        return ReviewsPage(productId: id);
                    },
                ),
            ],
        ),
        GoRoute(
            path: '/search',
            builder: (_, state) => SearchPage(q: state.uri.queryParameters['q']),
        ),
    ],
    errorBuilder: (_, state) => NotFoundPage(uri: state.uri),
);

class App extends StatelessWidget {
    @override
    Widget build(BuildContext context) =>
        MaterialApp.router(routerConfig: _router);
}

// Navigate by URL
context.go('/products/42');                 // replace stack
context.push('/products/42');                // push on stack
context.go('/products/42/reviews');
context.goNamed('product', pathParameters: {'id': '42'});
context.go('/search?q=cameras');

// 5) Bottom tab shell (preserve state per tab)
final _router2 = GoRouter(
    initialLocation: '/feed',
    routes: [
        StatefulShellRoute.indexedStack(
            builder: (context, state, navigationShell) =>
                ScaffoldWithNav(navigationShell: navigationShell),
            branches: [
                StatefulShellBranch(routes: [
                    GoRoute(path: '/feed',    builder: (_, __) => const FeedPage()),
                    GoRoute(path: '/feed/:id', builder: (_, s) => PostPage(id: s.pathParameters['id']!)),
                ]),
                StatefulShellBranch(routes: [
                    GoRoute(path: '/search',  builder: (_, __) => const SearchPage()),
                ]),
                StatefulShellBranch(routes: [
                    GoRoute(path: '/me',      builder: (_, __) => const ProfilePage()),
                ]),
            ],
        ),
    ],
);

class ScaffoldWithNav extends StatelessWidget {
    final StatefulNavigationShell navigationShell;
    const ScaffoldWithNav({super.key, required this.navigationShell});

    @override
    Widget build(BuildContext context) => Scaffold(
        body: navigationShell,
        bottomNavigationBar: NavigationBar(
            selectedIndex: navigationShell.currentIndex,
            onDestinationSelected: (i) => navigationShell.goBranch(i, initialLocation: i == navigationShell.currentIndex),
            destinations: const [
                NavigationDestination(icon: Icon(Icons.home),   label: 'Feed'),
                NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
                NavigationDestination(icon: Icon(Icons.person), label: 'Me'),
            ],
        ),
    );
}

// 6) Redirects and auth guards
final _router3 = GoRouter(
    refreshListenable: authState,                 // ChangeNotifier; router re-evaluates when notified
    redirect: (context, state) {
        final loggedIn = authState.isLoggedIn;
        final goingToLogin = state.uri.path == '/login';

        if (!loggedIn && !goingToLogin) return '/login?from=${state.uri}';
        if (loggedIn && goingToLogin)   return '/';
        return null;                                // no redirect
    },
    routes: [ /* … */ ],
);

// 7) Modal sheets and dialogs — keep them OUT of the router
showModalBottomSheet(context: context, builder: (_) => const FilterSheet());
showDialog(context: context, builder: (_) => const ConfirmDialog());
// These don't change the URL or affect deep linking — they're 'overlays', not 'destinations'.

// 8) Deep links — same routes as web URLs
// iOS: associated domains in entitlements
// Android: intent-filters with autoVerify="true" in AndroidManifest.xml
// go_router parses incoming deep links and matches the route table —
// you get the right page for free.

// 9) Pop with results, prevent back-press
final r = await context.push<bool>('/confirm');
if (r == true) deleteAccount();

class ConfirmExit extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        return PopScope(
            canPop: false,
            onPopInvoked: (didPop) async {
                if (didPop) return;
                final ok = await showDialog<bool>(
                    context: context,
                    builder: (_) => AlertDialog(
                        title: const Text('Exit?'),
                        actions: [
                            TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Stay')),
                            FilledButton(onPressed: () => Navigator.pop(context, true),  child: const Text('Exit')),
                        ],
                    ),
                );
                if (ok == true && context.mounted) Navigator.of(context).pop();
            },
            child: const Scaffold(/* … */),
        );
    }
}

// 10) Common bugs
//   • Calling Navigator after the widget is unmounted → use context.mounted check
//   • go_router path with leading slash inside nested route → resolves from root, not parent
//   • Forgetting MaterialApp.router with go_router → reverts to imperative behaviour
//   • Pushing onto a removed StatefulShellRoute branch → use goBranch, not go
//   • Modal route loses params on Hot Reload — restate from URL
//   • Deep link opens app but lands on home — make sure routes accept query and path params, redirect logic doesn't lose 'from'

Why it matters

Use go_router for any app you’ll ship: declarative routes, free deep linking, and StatefulShellRoute.indexedStack for the bottom-tab pattern that keeps each tab’s scroll position and navigation stack alive. Keep dialogs and bottom sheets outside the router — they’re overlays, not destinations, and treating them as routes causes back-button confusion.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
Navigator.push(
    context,
    MaterialPageRoute(builder: (_) => const ProfilePage()),
);
Navigator.pop(context);
// Or use go_router for declarative URLs.
Try it Yourself »

Exercise

Open a new page.

Navigator. (context, MaterialPageRoute(builder: (_) => const Profile()));

Test yourself

Q1. Push a new full-screen route with…
Q2. Common declarative routing package is…
Q3. Pop the current route with…

Discussion

Loading…