Routing Guide
This project uses GoRouter for declarative, type-safe navigation. All routes are centrally managed with support for nested routes, custom transitions, and parameter passing.
Structure
app/
└── routes.dart # Central route configurationfeatures/[feature]/presentation/pages/
├── feature_page.dart # Page with static name and path
├── feature_detail_page.dart # Page with parameters
└── feature_edit_page.dart # Page with optional parameters1. Route Configuration
All routes are defined in lib/app/routes.dart using GoRouter's declarative API.
Basic Route Setup
dart
import 'package:animations/animations.dart';
import 'package:flutter_boilerplate/app/di.dart';
import 'package:flutter_boilerplate/app/pages/home_page.dart';
import 'package:flutter_boilerplate/features/[feature]/presentation/pages/feature_page.dart';
import 'package:flutter_boilerplate/features/[feature]/presentation/pages/feature_detail_page.dart';
import 'package:go_router/go_router.dart';
/// Router for the app
final GoRouter router = createRouter();
/// Creates the router used by the app.
///
/// Defines all application routes with their paths, names, and builders.
/// Includes custom transitions and parameter handling.
GoRouter createRouter() => GoRouter(
observers: [
// Add analytics or other navigation observers
di<AnalyticsNavigatorObserver>(),
],
routes: [
// Simple route without parameters
GoRoute(
path: HomePage.path,
name: HomePage.name,
builder: (context, state) => const HomePage(),
),
// Route with custom transition
GoRoute(
path: FeaturePage.path,
name: FeaturePage.name,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const FeaturePage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
child: child,
);
},
),
),
// Route with required parameters
GoRoute(
path: '${FeatureDetailPage.path}/:itemId',
name: FeatureDetailPage.name,
builder: (context, state) {
final itemId = state.pathParameters['itemId']!;
return FeatureDetailPage(itemId: itemId);
},
),
// Route with optional parameters
GoRoute(
path: FeatureEditPage.path,
name: FeatureEditPage.name,
builder: (context, state) => const FeatureEditPage(),
),
GoRoute(
path: '${FeatureEditPage.path}/:itemId',
name: '${FeatureEditPage.name}Edit',
builder: (context, state) {
final itemId = state.pathParameters['itemId'];
return FeatureEditPage(itemId: itemId);
},
),
// Nested routes
GoRoute(
path: SettingsPage.path,
name: SettingsPage.name,
builder: (context, state) => const SettingsPage(),
routes: [
GoRoute(
path: ThemeSelectionPage.path,
name: ThemeSelectionPage.name,
builder: (context, state) => const ThemeSelectionPage(),
),
],
),
],
);Real Example (Notes Routes)
dart
GoRoute(
path: '${NoteDetailPage.path}/:noteId',
name: NoteDetailPage.name,
builder: (context, state) {
final noteId = state.pathParameters['noteId']!;
return NoteDetailPage(noteId: noteId);
},
),
GoRoute(
path: NoteEditPage.path,
name: NoteEditPage.name,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const NoteEditPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
),
),
GoRoute(
path: '${NoteEditPage.path}/:noteId',
name: '${NoteEditPage.name}Edit',
pageBuilder: (context, state) {
final noteId = state.pathParameters['noteId'];
return CustomTransitionPage(
key: state.pageKey,
child: NoteEditPage(noteId: noteId),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
);
},
),2. Page Definition
Every page must define static name and path constants for type-safe navigation.
Page Template
dart
import 'package:flutter/material.dart';
import 'package:flutter_boilerplate/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
/// Page for displaying [FeatureName] details.
///
/// Shows detailed information about a specific item and provides
/// options for editing and other actions.
class FeatureDetailPage extends StatelessWidget {
/// Creates a [FeatureDetailPage].
///
/// [itemId] - The unique identifier of the item to display.
const FeatureDetailPage({
required this.itemId,
super.key,
});
/// The unique identifier of the item to display.
final String itemId;
/// Name of the page
static const name = 'FeatureDetailPage';
/// Path of the page
static const path = '/feature-detail';
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10n.featureDetailTitle),
actions: [
IconButton(
onPressed: () => _navigateToEdit(context),
icon: const Icon(Icons.edit),
tooltip: l10n.editItemTooltip,
),
],
),
body: _buildBody(context),
);
}
/// Navigates to the edit page for this item.
void _navigateToEdit(BuildContext context) {
GoRouter.of(context).go('${FeatureEditPage.path}/$itemId');
}
Widget _buildBody(BuildContext context) {
// Implementation
return Container();
}
}Page with Optional Parameters
dart
/// Page for editing [FeatureName] items.
///
/// Can be used for both creating new items (when [itemId] is null)
/// and editing existing items (when [itemId] is provided).
class FeatureEditPage extends StatelessWidget {
/// Creates a [FeatureEditPage].
///
/// [itemId] - The unique identifier of the item to edit.
/// If null, creates a new item.
const FeatureEditPage({
this.itemId,
super.key,
});
/// The unique identifier of the item to edit.
/// If null, creates a new item.
final String? itemId;
/// Name of the page
static const name = 'FeatureEditPage';
/// Path of the page
static const path = '/feature-edit';
/// Whether this page is in edit mode (has an existing item).
bool get isEditMode => itemId != null;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(
isEditMode ? l10n.editFeatureTitle : l10n.createFeatureTitle,
),
),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context) {
// Implementation
return Container();
}
}Key Points for Page Definition
- Static Constants: Always define
nameandpathas static constants - Parameter Handling: Accept parameters through constructor
- Optional Parameters: Use nullable types for optional parameters
- Documentation: Document the page purpose and parameters
- Localization: Use localized strings for titles and labels
3. Navigation Patterns
Basic Navigation
dart
// ✅ Navigate to a simple page
GoRouter.of(context).go(FeaturePage.path);
// ✅ Navigate using named routes (preferred for type safety)
GoRouter.of(context).goNamed(FeaturePage.name);Navigation with Parameters
dart
// ✅ Navigate with path parameters
GoRouter.of(context).go('${FeatureDetailPage.path}/$itemId');
// ✅ Navigate with named route and parameters
GoRouter.of(context).goNamed(
FeatureDetailPage.name,
pathParameters: {'itemId': itemId},
);Navigation with Query Parameters
dart
// ✅ Navigate with query parameters
GoRouter.of(context).goNamed(
FeaturePage.name,
queryParameters: {
'filter': 'active',
'sort': 'date',
},
);
// Access query parameters in the page
class FeaturePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final state = GoRouterState.of(context);
final filter = state.uri.queryParameters['filter'];
final sort = state.uri.queryParameters['sort'];
return Scaffold(/* ... */);
}
}Push vs Go Navigation
dart
// ✅ Replace current page (go)
GoRouter.of(context).go(FeaturePage.path);
// ✅ Push new page on stack (push)
GoRouter.of(context).push(FeatureDetailPage.path);
// ✅ Pop current page
GoRouter.of(context).pop();
// ✅ Pop with result
GoRouter.of(context).pop(result);Context Usage (Following AGENTS.md Rules)
dart
// ❌ Don't use context extensions
context.go('/path');
context.push('/path');
// ✅ Use explicit GoRouter methods
GoRouter.of(context).go('/path');
GoRouter.of(context).push('/path');4. Custom Transitions
Predefined Transitions
dart
GoRoute(
path: FeaturePage.path,
name: FeaturePage.name,
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const FeaturePage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// ✅ Shared axis transition (Material Design)
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
),
),Available Transition Types
dart
// Material Design transitions from animations package
SharedAxisTransition(
transitionType: SharedAxisTransitionType.horizontal, // Left/right
transitionType: SharedAxisTransitionType.vertical, // Up/down
transitionType: SharedAxisTransitionType.scaled, // Scale in/out
);
FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
FadeScaleTransition(
animation: animation,
child: child,
);Custom Transitions
dart
GoRoute(
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: const FeaturePage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// ✅ Custom slide transition
return SlideTransition(
position: animation.drive(
Tween(begin: const Offset(1.0, 0.0), end: Offset.zero),
),
child: child,
);
},
),
),5. Route Guards and Redirects
Authentication Guard
dart
GoRouter createRouter() => GoRouter(
redirect: (context, state) {
final isAuthenticated = di<AuthService>().isAuthenticated;
final isAuthRoute = state.uri.path == AuthPage.path;
// Redirect to auth if not authenticated and not already on auth page
if (!isAuthenticated && !isAuthRoute) {
return AuthPage.path;
}
// Redirect to home if authenticated and on auth page
if (isAuthenticated && isAuthRoute) {
return HomePage.path;
}
// No redirect needed
return null;
},
routes: [/* routes */],
);Onboarding Guard
dart
redirect: (context, state) {
final hasCompletedOnboarding = di<OnboardingService>().hasCompleted;
final isOnboardingRoute = state.uri.path == OnboardingPage.path;
if (!hasCompletedOnboarding && !isOnboardingRoute) {
return OnboardingPage.path;
}
return null;
},6. Error Handling
404 Error Page
dart
GoRouter createRouter() => GoRouter(
errorBuilder: (context, state) => ErrorPage(
error: state.error,
),
routes: [/* routes */],
);Route Validation
dart
GoRoute(
path: '${FeatureDetailPage.path}/:itemId',
name: FeatureDetailPage.name,
builder: (context, state) {
final itemId = state.pathParameters['itemId'];
// ✅ Validate required parameters
if (itemId == null || itemId.isEmpty) {
return const ErrorPage(
error: 'Invalid item ID',
);
}
return FeatureDetailPage(itemId: itemId);
},
),7. Testing Routes
Route Testing
dart
void main() {
group('Router', () {
testWidgets('should navigate to feature page', (tester) async {
final router = createRouter();
await tester.pumpWidget(
MaterialApp.router(
routerConfig: router,
),
);
// Navigate to feature page
router.go(FeaturePage.path);
await tester.pumpAndSettle();
// Verify navigation
expect(find.byType(FeaturePage), findsOneWidget);
});
testWidgets('should handle parameters correctly', (tester) async {
final router = createRouter();
const testId = 'test-123';
await tester.pumpWidget(
MaterialApp.router(
routerConfig: router,
),
);
// Navigate with parameter
router.go('${FeatureDetailPage.path}/$testId');
await tester.pumpAndSettle();
// Verify parameter was passed
final page = tester.widget<FeatureDetailPage>(
find.byType(FeatureDetailPage),
);
expect(page.itemId, testId);
});
});
}Navigation Testing in Widgets
dart
void main() {
group('FeaturePage', () {
testWidgets('should navigate to detail page on item tap', (tester) async {
final mockGoRouter = MockGoRouter();
await tester.pumpWidget(
MaterialApp(
home: InheritedGoRouter(
goRouter: mockGoRouter,
child: const FeaturePage(),
),
),
);
// Tap on an item
await tester.tap(find.byType(FeatureWidget).first);
await tester.pumpAndSettle();
// Verify navigation was called
verify(mockGoRouter.go(any)).called(1);
});
});
}8. Best Practices
Route Organization
- Central Definition: Define all routes in
lib/app/routes.dart - Static Constants: Use static
nameandpathconstants in pages - Type Safety: Use named routes with parameters for type safety
- Consistent Naming: Use consistent naming patterns for routes
Parameter Handling
- Required Parameters: Use non-nullable types for required parameters
- Optional Parameters: Use nullable types for optional parameters
- Validation: Validate parameters in route builders
- Error Handling: Handle invalid parameters gracefully
Navigation
- Context Usage: Use
GoRouter.of(context)instead of context extensions - Named Routes: Prefer named routes over path-based navigation
- Parameter Safety: Use
pathParametersfor type-safe parameter passing - State Management: Consider navigation state in your app's state management
Performance
- Lazy Loading: Use lazy loading for heavy pages
- Transition Optimization: Choose appropriate transitions for UX
- Route Caching: Leverage GoRouter's built-in route caching
- Memory Management: Properly dispose of resources in pages
The routing system provides a robust foundation for navigation while maintaining type safety and good user experience. Keep routes organized and well-documented for maintainability.