Flutter Advanced Navigation: Master GoRouter | (Ch. 14)
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/42and have the app open directly to Product 42? -
Web Support: On the web, users expect the URL bar to change when they navigate.
Navigator.pushdoesn'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.
📖 Chapter 14: Table of Contents
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:
- A Home Screen (List of products).
- A Details Screen (Shows the Product ID passed via URL).
- 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
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.
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.
/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
Post a Comment