Router API
Understand the modern routing system in Flutter using the Router API.
What is it?
The Router API is a modern navigation system introduced in Flutter that provides more control and flexibility than the traditional Navigator 1.0 approach. It uses a declarative routing model where routes are defined in a configuration object and can be dynamically updated based on the app state, enabling features like deep linking, URL-based navigation, and more complex routing scenarios.
Why does it exist?
The Router API exists to:
- Support declarative routing
- Enable URL-based navigation
- Handle deep linking elegantly
- Manage complex navigation flows
- Support web and desktop navigation
- Provide more control over routing
- Enable dynamic route updates
Router vs Navigator
Comparing Router API with traditional Navigator.
// Traditional Navigator (1.0)
class Navigator1Example extends StatelessWidget {
const Navigator1Example({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Navigator 1.0',
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/detail': (context) => const DetailScreen(),
},
);
}
}
// Modern Router API (2.0)
class RouterApiExample extends StatelessWidget {
const RouterApiExample({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Router API',
routerConfig: router,
);
}
}
// Router configuration
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/detail',
builder: (context, state) => const DetailScreen(),
),
],
);
// Router properties:
// 1. initialLocation - Starting route
// 2. routes - List of route definitions
// 3. redirect - Redirect logic
// 4. errorBuilder - Error handling
What's happening here? - Router API is declarative - Navigator 1.0 is imperative - Router supports URL-based navigation - Router enables deep linking
GoRouter Basics
GoRouter is the most common Router API implementation.
// Basic GoRouter setup
import 'package:go_router/go_router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'GoRouter Demo',
routerConfig: _router,
);
}
}
// Define router
final _router = GoRouter(
initialLocation: '/',
routes: [
// Home route
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
// Detail route
GoRoute(
path: '/detail',
builder: (context, state) => const DetailScreen(),
),
// Profile route with parameter
GoRoute(
path: '/profile/:id',
builder: (context, state) {
final id = state.pathParameters['id'] ?? '0';
return ProfileScreen(userId: int.parse(id));
},
),
],
);
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Navigate using GoRouter
ElevatedButton(
onPressed: () {
context.go('/detail');
},
child: const Text('Go to Detail'),
),
ElevatedButton(
onPressed: () {
context.go('/profile/123');
},
child: const Text('Go to Profile'),
),
],
),
),
);
}
}
What's happening here? - GoRouter defines routes declaratively - context.go() navigates to routes - Path parameters with :id syntax - State contains route information
Nested Routes
Nesting routes with GoRouter.
// Nested routes with GoRouter
class NestedRoutesApp extends StatelessWidget {
const NestedRoutesApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _nestedRouter,
);
}
}
final _nestedRouter = GoRouter(
initialLocation: '/',
routes: [
// Root route with ShellBranch for nested navigation
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
// Home tab
GoRoute(
path: '/',
builder: (context, state) => const HomeTab(),
),
// Settings tab
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsTab(),
),
// Profile tab with nested routes
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileTab(),
routes: [
GoRoute(
path: 'edit',
builder: (context, state) => const EditProfileScreen(),
),
GoRoute(
path: 'settings',
builder: (context, state) => const ProfileSettingsScreen(),
),
],
),
],
),
],
);
// Scaffold with bottom navigation
class ScaffoldWithNavBar extends StatelessWidget {
const ScaffoldWithNavBar({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Settings'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
onTap: (index) {
switch (index) {
case 0:
context.go('/');
break;
case 1:
context.go('/settings');
break;
case 2:
context.go('/profile');
break;
}
},
),
);
}
}
What's happening here? - ShellRoute provides persistent layout - Nested routes under tabs - context.go() updates navigation - BottomNavigationBar integration
Redirects and Guards
Redirecting routes with GoRouter.
// Route guards and redirects
class AuthState extends ChangeNotifier {
bool _isLoggedIn = false;
bool get isLoggedIn => _isLoggedIn;
void login() {
_isLoggedIn = true;
notifyListeners();
}
void logout() {
_isLoggedIn = false;
notifyListeners();
}
}
class GuardedRoutesApp extends StatelessWidget {
const GuardedRoutesApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthState(),
child: MaterialApp.router(
routerConfig: _guardedRouter,
),
);
}
}
final _guardedRouter = GoRouter(
initialLocation: '/',
redirect: (context, state) {
// Check auth state
final auth = Provider.of<AuthState>(context);
final isLoggedIn = auth.isLoggedIn;
final isLoginRoute = state.matchedLocation == '/login';
// Redirect to login if not authenticated
if (!isLoggedIn && !isLoginRoute) {
return '/login';
}
// Redirect to home if already logged in
if (isLoggedIn && isLoginRoute) {
return '/';
}
return null; // No redirect
},
routes: [
// Public routes
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
// Protected routes
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsScreen(),
),
],
),
],
errorBuilder: (context, state) => const ErrorScreen(),
);
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: ElevatedButton(
onPressed: () {
// Login and navigate to home
context.read<AuthState>().login();
context.go('/');
},
child: const Text('Login'),
),
),
);
}
}
What's happening here? - redirect controls navigation flow - Auth state determines routes - Login redirects to home - Protected routes require auth
Passing Data with GoRouter
Passing data between routes.
// Passing data with GoRouter
class DataRoutingApp extends StatelessWidget {
const DataRoutingApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _dataRouter,
);
}
}
final _dataRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const DataHomeScreen(),
),
GoRoute(
path: '/detail',
builder: (context, state) {
// Extract data from state
final extra = state.extra as Map<String, dynamic>?;
return DetailScreen(
title: extra?['title'] ?? 'Default',
count: extra?['count'] ?? 0,
);
},
),
GoRoute(
path: '/user/:id',
builder: (context, state) {
// Extract path parameter
final id = state.pathParameters['id'] ?? '0';
// Extract query parameters
final name = state.uri.queryParameters['name'] ?? '';
return UserScreen(
id: int.parse(id),
name: name,
);
},
),
],
);
class DataHomeScreen extends StatelessWidget {
const DataHomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Data Routing')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Pass data via extra
ElevatedButton(
onPressed: () {
context.push(
'/detail',
extra: {
'title': 'Hello World',
'count': 42,
},
);
},
child: const Text('Go to Detail (Extra)'),
),
// Pass data via path and query
ElevatedButton(
onPressed: () {
context.push('/user/123?name=John');
},
child: const Text('Go to User (Path/Query)'),
),
],
),
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({
super.key,
required this.title,
required this.count,
});
final String title;
final int count;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Detail')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Title: $title'),
Text('Count: $count'),
ElevatedButton(
onPressed: () {
context.pop();
},
child: const Text('Go Back'),
),
],
),
),
);
}
}
What's happening here? - extra: pass arbitrary data - Path parameters: /user/:id - Query parameters: ?name=John - state.uri for URL parsing
Real-World Examples
Common patterns with Router API.
// 1. E-commerce app routing
class EcommerceRouter {
static final router = GoRouter(
initialLocation: '/home',
routes: [
// Main shell
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(
currentIndex: _getCurrentIndex(state),
child: child,
);
},
routes: [
// Home tab
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
// Search tab
GoRoute(
path: '/search',
builder: (context, state) => const SearchScreen(),
),
// Cart tab
GoRoute(
path: '/cart',
builder: (context, state) => const CartScreen(),
),
// Profile tab
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
),
// Product details (full screen)
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id'] ?? '0';
return ProductDetailScreen(productId: int.parse(id));
},
),
// Checkout flow
GoRoute(
path: '/checkout',
builder: (context, state) => const CheckoutScreen(),
routes: [
GoRoute(
path: 'shipping',
builder: (context, state) => const ShippingScreen(),
),
GoRoute(
path: 'payment',
builder: (context, state) => const PaymentScreen(),
),
GoRoute(
path: 'confirmation',
builder: (context, state) => const OrderConfirmationScreen(),
),
],
),
],
errorBuilder: (context, state) => const NotFoundScreen(),
);
static int _getCurrentIndex(GoRouterState state) {
final location = state.matchedLocation;
if (location.contains('/home')) return 0;
if (location.contains('/search')) return 1;
if (location.contains('/cart')) return 2;
if (location.contains('/profile')) return 3;
return 0;
}
}
// 2. Deep linking support
class DeepLinkingApp extends StatelessWidget {
const DeepLinkingApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _deepLinkRouter,
);
}
}
final _deepLinkRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id'] ?? '0';
return ProductDetailScreen(productId: int.parse(id));
},
),
GoRoute(
path: '/user/:userId/post/:postId',
builder: (context, state) {
final userId = state.pathParameters['userId'] ?? '0';
final postId = state.pathParameters['postId'] ?? '0';
return PostScreen(
userId: int.parse(userId),
postId: int.parse(postId),
);
},
),
],
// Handle web deep links
urlPathStrategy: UrlPathStrategy.path,
);
What's happening here? - ShellRoute with bottom nav - Nested checkout flow - Deep linking support - Path parameter extraction
Best Practices
Use ShellRoute for Persistent Layout
// Good - ShellRoute for navigation
ShellRoute(
builder: (context, state, child) {
return PersistentLayout(child: child);
},
routes: [...],
)
// Bad - Redundant layouts
GoRoute(
path: '/',
builder: (context, state) => const PersistentLayout(child: HomeScreen()),
)
Use Redirects for Auth
// Good - Auth redirect
redirect: (context, state) {
final auth = Provider.of<AuthState>(context);
if (!auth.isLoggedIn) return '/login';
return null;
}
Use Extra for Complex Data
// Good - Extra for complex data
context.push('/detail', extra: userData);
// Bad - Encoding in path
context.push('/detail/${userData.id}/${userData.name}');
Common Mistakes
Not Handling Errors
Wrong:
// No error handling
GoRouter(
routes: [...],
// No errorBuilder
)
Correct:
// With error handling
GoRouter(
routes: [...],
errorBuilder: (context, state) => const ErrorScreen(),
)
Missing Redirects
Wrong:
// No auth redirect
GoRouter(
routes: [
GoRoute(path: '/login', ...),
GoRoute(path: '/profile', ...), // Can access without auth
],
)
Correct:
// With auth redirect
GoRouter(
redirect: (context, state) {
if (!auth.isLoggedIn) return '/login';
return null;
},
routes: [...],
)
Summary
The Router API provides a modern, declarative approach to navigation in Flutter. GoRouter is the most popular implementation, offering features like nested routes, redirects, path parameters, and deep linking support. Use it for complex navigation flows and web applications.
Next Steps
Did You Know?
- Router API is declarative
- GoRouter supports nested routes
- Redirects control navigation flow
- ShellRoute provides persistent layouts
- Path parameters use :id syntax
- Extra data can be passed with navigation
- Deep linking is built-in
- Router API is the future of Flutter navigation