Flutter Advanced Navigation: Master GoRouter | (Ch. 14)

Flutter Course Chapter 14: Advanced Navigation, showing a developer at a workstation with illustrations of navigation signposts and route maps representing GoRouter.

Welcome to Chapter 14!

In the last chapter, we mastered Navigator.push and Navigator.pop. This "imperative" style (meaning you give commands like "go here" and "go back") is perfect for small apps.

But as your app grows, you will hit some walls:

  • Deep Linking: What if you want to send a user a link like myapp.com/products/42 and have the app open directly to Product 42?
  • Web Support: On the web, users expect the URL bar to change when they navigate. Navigator.push doesn't do this easily.
  • Complex Logic: Redirecting users if they aren't logged in (Authentication Guards) becomes messy with basic navigation.

Enter GoRouter.

GoRouter is a package maintained by the Google Flutter team that solves all these problems. It uses a "declarative" routing system based on URLs. It is the modern standard for Flutter navigation.

1. The "Old" Way: Named Routes

Before GoRouter, Flutter offered "Named Routes." You might see this in older tutorials. It looks like this:


// Don't use this anymore!
MaterialApp(
  routes: {
    '/': (context) => HomeScreen(),
    '/details': (context) => DetailScreen(),
  },
)

You would navigate using Navigator.pushNamed(context, '/details').

Why we are skipping this: While simple, passing arguments (data) to these routes is clunky and non-intuitive. It also struggles with nested navigation and deep links. We are going straight to the professional solution.

2. Why GoRouter? (The Modern Standard)

GoRouter treats your app screens like URLs on a website.

  • URL-based: Every screen has a path (e.g., /settings/profile).
  • Backwards Compatibility: It still uses the native navigation stack under the hood.
  • Redirection: Easily redirect users (e.g., if not logged in, send to /login).
  • Web Ready: It automatically syncs with the browser URL bar.

Step 1: Installation & Setup

Since GoRouter is a package, we need to add it to our project.

1. Open your pubspec.yaml file.
2. Add go_router under dependencies.


dependencies:
  flutter:
    sdk: flutter
  go_router: ^12.0.0  # Check pub.dev for the latest version

3. Save the file (or run flutter pub get in terminal).
4. Restart your app.

Step 2: Defining Routes

GoRouter requires us to define a "configuration" object that lists all the possible places a user can go.

The Configuration Object

Usually, we create this variable in `main.dart` or a separate `router.dart` file.


import 'package:go_router/go_router.dart';

// The configuration
final GoRouter _router = GoRouter(
  initialLocation: '/', // Where to start
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/details',
      builder: (context, state) => const DetailScreen(),
    ),
  ],
);

MaterialApp.router

Now, we need to tell our MaterialApp to use this router. We change the constructor from MaterialApp to MaterialApp.router.


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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router( // Note the .router here!
      routerConfig: _router, // Pass in our config
      title: 'GoRouter Demo',
    );
  }
}

Step 3: Navigating with GoRouter

Now that it's set up, how do we move around? GoRouter gives us two main methods: go and push.

`context.go()` vs `context.push()`

This is the most critical concept to understand.

1. context.go('/details')

  • This is a JUMP.
  • It discards the current stack and jumps to the target URL.
  • If you define your routes structure carefully, it can build a back stack for you, but generally, use this when switching between major sections (like using a BottomNavigationBar).

2. context.push('/details')

  • This is a STACK ADDITION.
  • It puts the new page on top of the current page.
  • This preserves the "Back" button arrow.
  • Use this for "drilling down" into content (e.g., clicking a product in a list).

// Inside a button
ElevatedButton(
  onPressed: () {
    // Navigate to the details page
    context.push('/details');
  },
  child: const Text('Go to Details'),
)

Step 4: Passing Parameters (Dynamic URLs)

What if we have 100 products? We can't define 100 routes like /product1, /product2.

We use Path Parameters with a colon :.

Path Parameters (`:id`)

In your router config, define a dynamic segment:


    GoRoute(
      path: '/product/:id', // The :id is a placeholder
      builder: (context, state) {
        // We read the ID from the state
        final String id = state.pathParameters['id']!;
        return ProductScreen(id: id);
      },
    ),

Reading Parameters

Now, you can navigate by passing the actual ID:


// Navigate to Product 42
context.push('/product/42');

GoRouter will match this to /product/:id, extract "42", and pass it to your ProductScreen.

Step 5: Error Handling (404 Pages)

If a user tries to go to a URL that doesn't exist (e.g., typing /garbage in the web browser), the app shouldn't crash.

GoRouter lets you define a custom Error Screen.


final GoRouter _router = GoRouter(
  initialLocation: '/',
  errorBuilder: (context, state) => const ErrorScreen(), // Your custom 404 page
  routes: [ ... ],
);

Full Example: A "Product Store" App

Let's put it all together. This app has:

  1. A Home Screen (List of products).
  2. A Details Screen (Shows the Product ID passed via URL).
  3. A 404 Error Screen.

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

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

// 1. Define the Router
final GoRouter _router = GoRouter(
  initialLocation: '/',
  errorBuilder: (context, state) => const ErrorScreen(),
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        // Nesting routes allows for clearer structure
        // This path becomes /product/:id
        GoRoute(
          path: 'product/:id', 
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return ProductScreen(id: id);
          },
        ),
      ],
    ),
  ],
);

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
      title: 'GoRouter Store',
    );
  }
}

// 2. Home Screen
class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Store Home')),
      body: ListView(
        children: [
          ListTile(
            title: const Text('Laptop'),
            trailing: const Icon(Icons.arrow_forward),
            onTap: () => context.push('/product/laptop-001'),
          ),
          ListTile(
            title: const Text('Phone'),
            trailing: const Icon(Icons.arrow_forward),
            onTap: () => context.push('/product/phone-999'),
          ),
          ListTile(
            title: const Text('Watch'),
            trailing: const Icon(Icons.arrow_forward),
            onTap: () => context.push('/product/watch-xt'),
          ),
          // Test the error page
          ListTile(
            title: const Text('Broken Link (Test 404)'),
            tileColor: Colors.red.shade100,
            onTap: () => context.push('/garbage-url'),
          ),
        ],
      ),
    );
  }
}

// 3. Product Screen
class ProductScreen extends StatelessWidget {
  final String id;
  const ProductScreen({super.key, required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product: $id')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.shopping_bag, size: 80, color: Colors.blue),
            SizedBox(height: 20),
            Text('Viewing details for item:', style: TextStyle(fontSize: 18)),
            Text(id, style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold)),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/'), // Jump back home
              child: const Text('Back to Home'),
            ),
          ],
        ),
      ),
    );
  }
}

// 4. Error Screen
class ErrorScreen extends StatelessWidget {
  const ErrorScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Page Not Found')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error, size: 80, color: Colors.red),
            const SizedBox(height: 20),
            const Text('404 - Oops!'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => context.go('/'),
              child: const Text('Go Home'),
            ),
          ],
        ),
      ),
    );
  }
}

❓ FAQ: Frequently Asked Questions

Q: When should I use `go()` vs `push()`?
Use `push()` when you are drilling deeper into a specific flow (like Home -> List -> Item). This keeps the "Back" arrow working normally.

Use `go()` when you are switching major contexts (like switching tabs, or going from Login -> Home). `go()` modifies the underlying stack to match the URL, which might remove the back button if not configured with nested routes.
Q: Can I pass complex objects (like a User class) instead of just an ID string?
Technically yes, using the `extra` parameter: context.push('/path', extra: myUserObject).
However, be careful: If a user refreshes the page (on web) or deep links into the app, that "extra" object will be lost (it will be null). It is safer to pass an ID (like /user/123) and fetch the user data again on the new screen.
Q: How do I handle Login/Authentication redirects?
GoRouter has a `redirect` callback in its configuration. You can check if a user is logged in. If not, and they try to access /profile, you can return /login to automatically redirect them.

Conclusion

You have now graduated to modern Flutter navigation.

GoRouter might seem like a bit more setup initially than Navigator.push, but it pays off massively as your app grows. It gives you deep linking, web support, and organized code structure for free.

In the next chapter, we are going to build Project 3: A Multi-Screen Quiz App. We will use GoRouter to navigate from the "Welcome" screen, through several "Question" screens, and finally to a "Results" screen that calculates your score.

Comments