Modern Navigation with GoRouter in Flutter
Modern Navigation with GoRouter in Flutter
What You’ll Learn
Master declarative navigation using GoRouter, Flutter’s recommended routing solution. Learn to handle complex navigation scenarios like deep linking, nested routes, and navigation guards with clean, maintainable code.
Why GoRouter?
Flutter’s imperative Navigator 1.0 works for simple apps, but complex navigation quickly becomes messy with push/pop logic. GoRouter brings:
- Declarative routing: Define routes upfront, navigate by path
- Deep linking: Handle URLs automatically (web & mobile)
- Type safety: Compile-time checked route parameters
- Nested navigation: Multiple navigators (bottom nav, tabs)
- Redirect logic: Authentication guards, onboarding flows
Getting Started
Add GoRouter to pubspec.yaml:
dependencies:
go_router: ^13.0.0
Example: Basic App Navigation
Let’s build a simple app with three screens:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'GoRouter Demo',
);
}
// Define all routes in one place
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsScreen(),
),
],
);
}
// Home Screen
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => context.go('/profile'),
child: const Text('Go to Profile'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.push('/settings'),
child: const Text('Push Settings'),
),
],
),
),
);
}
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: Center(
child: ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('Back to Home'),
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Center(
child: ElevatedButton(
onPressed: () => context.pop(),
child: const Text('Pop Back'),
),
),
);
}
}
Key methods:
context.go('/path')- Replace current location (like replacement)context.push('/path')- Push onto stack (adds back button)context.pop()- Go back
Route Parameters
Pass data between screens using path and query parameters:
GoRoute(
path: '/user/:id', // Path parameter
builder: (context, state) {
final userId = state.pathParameters['id']!;
final tab = state.uri.queryParameters['tab'] ?? 'posts';
return UserScreen(
userId: userId,
initialTab: tab,
);
},
),
// Navigate with parameters
context.go('/user/123?tab=followers');
Type-safe alternative with extras:
GoRoute(
path: '/product',
builder: (context, state) {
final product = state.extra as Product;
return ProductScreen(product: product);
},
),
// Pass complex objects
context.push('/product', extra: myProduct);
Nested Navigation with ShellRoute
Perfect for apps with bottom navigation or tab bars:
final router = GoRouter(
initialLocation: '/home',
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(
path: '/home',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/explore',
builder: (context, state) => ExploreScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
],
),
// Routes outside shell (full screen)
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(),
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({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.path;
if (location.startsWith('/home')) return 0;
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');
break;
case 1:
context.go('/explore');
break;
case 2:
context.go('/profile');
break;
}
}
}
Authentication Guard with Redirects
Protect routes and redirect unauthenticated users:
class AuthState extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void login() {
_isAuthenticated = true;
notifyListeners();
}
void logout() {
_isAuthenticated = false;
notifyListeners();
}
}
final authState = AuthState();
final router = GoRouter(
refreshListenable: authState, // Rebuild routes when auth changes
redirect: (context, state) {
final isAuthenticated = authState.isAuthenticated;
final isGoingToLogin = state.matchedLocation == '/login';
// Redirect to login if not authenticated
if (!isAuthenticated && !isGoingToLogin) {
return '/login';
}
// Redirect to home if already logged in
if (isAuthenticated && isGoingToLogin) {
return '/';
}
return null; // No redirect needed
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => LoginScreen(),
),
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
),
],
);
Error Handling
Handle 404s and invalid routes gracefully:
final router = GoRouter(
routes: [...],
errorBuilder: (context, state) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text(
'Page not found: ${state.uri.path}',
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/'),
child: const Text('Go Home'),
),
],
),
),
);
},
);
Try It Yourself
Build a blog app with GoRouter:
- Create routes for home, post list, and post detail (with ID parameter)
- Add a ShellRoute with bottom navigation (Home, Favorites, Profile)
- Implement redirect logic to show onboarding screen for first-time users
- Handle deep links like
/posts/123to open specific posts
Challenge: Add route transitions using CustomTransitionPage:
GoRoute(
path: '/profile',
pageBuilder: (context, state) {
return CustomTransitionPage(
child: ProfileScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
);
},
),
Tip of the Day
Debugging routes: Enable logging to see navigation events:
GoRouter(
debugLogDiagnostics: true,
routes: [...],
);
Named routes: For better refactoring, define route names as constants:
class AppRoutes {
static const home = '/';
static const profile = '/profile';
static const user = '/user/:id';
}
context.go(AppRoutes.profile);
Web support: GoRouter automatically handles browser back/forward buttons and updates the URL bar. Your Flutter app becomes a true web app with bookmarkable URLs!