Flutter Lesson 35 of 83 4 min read
Routing with go_router in Flutter
Set up URL-based navigation in Flutter with go_router: routes, path and query parameters, redirects for login, and nested navigation.
On this page
Navigator.push is simple, but it knows nothing about URLs. As an app grows you need:
- A browser address bar that works on the web.
- Deep links: opening
myapp.com/product/42straight to that product. - Redirecting to login when the user is signed out.
- One place where every screen is listed.
The go_router package, maintained by the Flutter team, provides all of this.
flutter pub add go_router
Defining routes #
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'product/:id', // becomes /product/:id
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductPage(id: id);
},
),
],
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),
),
],
);
void main() => runApp(MaterialApp.router(routerConfig: router));
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => context.push('/settings'),
),
],
),
body: ListView(
children: [
for (final id in ['1', '2', '3'])
ListTile(
title: Text('Product $id'),
onTap: () => context.go('/product/$id'),
),
],
),
);
}
}
class ProductPage extends StatelessWidget {
const ProductPage({super.key, required this.id});
final String id;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $id')),
body: Center(child: Text('Details for product $id')),
);
}
}
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: const Text('Settings')));
}
}
Use MaterialApp.router with routerConfig. There is no home.
go or push? #
| Call | Effect |
|---|---|
context.go('/product/7') | Jump to that location. The stack is rebuilt from the route tree, so back goes to the parent route |
context.push('/settings') | Put the page on top of whatever is showing. Back returns to it |
context.pop() | Go back. Can carry a result: context.pop(value) |
context.replace('/home') | Swap the current page |
Use go for moving around the app’s structure and push for a temporary page.
Parameters #
// Path parameter: /product/42
GoRoute(
path: '/product/:id',
builder: (context, state) => ProductPage(id: state.pathParameters['id']!),
)
// Query parameters: /search?q=shoes&sort=price
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final sort = state.uri.queryParameters['sort'] ?? 'relevance';
return SearchPage(query: query, sort: sort);
},
)
// Build the address safely:
context.go(Uri(path: '/search', queryParameters: {'q': 'shoes'}).toString());
Values from a URL are always strings. Convert and validate them, since a user can type anything in the address bar.
Named routes #
Names protect you from typos in paths and from breaking links when a path changes.
GoRoute(
name: 'product',
path: '/product/:id',
builder: (context, state) => ProductPage(id: state.pathParameters['id']!),
)
context.goNamed('product', pathParameters: {'id': '42'});
Redirects: protecting pages #
redirect runs before every navigation. Return a new location, or null to continue.
final router = GoRouter(
refreshListenable: authState, // a ChangeNotifier: re-check when login state changes
redirect: (context, state) {
final loggedIn = authState.isLoggedIn;
final goingToLogin = state.matchedLocation == '/login';
if (!loggedIn && !goingToLogin) return '/login';
if (loggedIn && goingToLogin) return '/';
return null;
},
routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
],
);
When the user logs in or out, authState notifies and the router applies the redirect again automatically.
A page for unknown addresses #
GoRouter(
errorBuilder: (context, state) => Scaffold(
body: Center(child: Text('Nothing found at ${state.uri}')),
),
routes: const [], // your routes here
)
Bottom navigation with its own stack per tab #
StatefulShellRoute keeps the navigation bar on screen and gives each tab its own history.
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => Scaffold(
body: navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: navigationShell.currentIndex,
onDestinationSelected: navigationShell.goBranch,
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
),
),
branches: [
StatefulShellBranch(routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
]),
StatefulShellBranch(routes: [
GoRoute(path: '/profile', builder: (context, state) => const ProfilePage()),
]),
],
)
Passing objects #
context.push('/edit', extra: note) passes any object, read with state.extra as Note. It is convenient, but the object is lost if the page is opened from a link or refreshed in a browser. Prefer passing an id in the path.
Try it yourself #
Convert a two-screen app to go_router. Add a /login route and a simple ChangeNotifier with an isLoggedIn flag. Redirect to login when signed out, and back to home after signing in.