Overview
Nest Flutter provides seamless integration with GoRouter through the RouteMixin. Routes are defined alongside dependency injection providers in modules, creating a cohesive architecture where routing and services are organized together.
Route Collection Mechanism
The route collection system automatically gathers routes from all modules in your application hierarchy.
Collection Flow
- Start at root module:
Modular.router() begins with the root module
- Depth-first traversal: Recursively processes all imported modules
- Apply prefixes: Each module’s
routePrefix is applied to its routes
- Prevent duplicates: Module types are tracked to prevent duplicate processing
- Flatten structure: All routes are collected into a single flat list
- Create router: The flat list is passed to GoRouter
Visual Example
AppModule
├── routes: [/]
├── imports:
│ ├── AuthModule
│ │ ├── routePrefix: /auth
│ │ └── routes: [/login, /register] → [/auth/login, /auth/register]
│ ├── UserModule
│ │ ├── routePrefix: /users
│ │ └── routes: [/, /:id] → [/users, /users/:id]
│ └── AdminModule
│ ├── routePrefix: /admin
│ └── routes: [/dashboard, /settings] → [/admin/dashboard, /admin/settings]
Final routes: [/, /auth/login, /auth/register, /users, /users/:id, /admin/dashboard, /admin/settings]
Collection Algorithm
List<RouteBase> collectAllRoutes([Set<Type>? processedModuleTypes]) {
processedModuleTypes ??= <Type>{};
final allRoutes = <RouteBase>[];
// Skip if this module type has already been processed
if (processedModuleTypes.contains(runtimeType)) {
return allRoutes; // Return empty list for duplicate module types
}
// Mark this module type as processed
processedModuleTypes.add(runtimeType);
// First, collect routes from imported modules
for (final importedModule in imports) {
if (importedModule is Module) {
allRoutes.addAll(importedModule.collectAllRoutes(processedModuleTypes));
}
}
// Then add this module's routes, applying prefix if specified
for (final route in routes) {
final processedRoute = (routePrefix != null && routePrefix!.isNotEmpty)
? _applyRoutePrefix(route, routePrefix!)
: route;
allRoutes.add(processedRoute);
}
return allRoutes;
}
Route Prefixing
Prefix Application
Route prefixes are applied automatically to all routes in a module.
Prefix Normalization Rules
-
Prefix normalization:
- Always starts with
/
- Never ends with
/
- Empty strings are ignored
-
Route path normalization:
- Always starts with
/
- Combined with prefix avoiding double slashes
-
Special handling for root
/:
- If route path is
/, the result is just the prefix
Prefix Examples
class ExamplesModule extends Module {
@override
String get routePrefix => '/api/v1';
@override
List<RouteBase> get routes => [
GoRoute(path: '/', builder: ...), // → /api/v1
GoRoute(path: '/users', builder: ...), // → /api/v1/users
GoRoute(path: 'products', builder: ...), // → /api/v1/products
];
}
Prefix Variations
All these variations produce the same result:
// These all become: /api
routePrefix => '/api';
routePrefix => 'api';
routePrefix => '/api/';
routePrefix => 'api/';
// Combined with path '/users' all produce: /api/users
path: '/users';
path: 'users';
Nested Prefixes
Each module’s prefix is independent. Nested modules don’t inherit parent prefixes.
class ParentModule extends Module {
@override
String get routePrefix => '/parent';
@override
List<Module> get imports => [ChildModule()];
@override
List<RouteBase> get routes => [
GoRoute(path: '/home', builder: ...), // → /parent/home
];
}
class ChildModule extends Module {
@override
String get routePrefix => '/child';
@override
List<RouteBase> get routes => [
GoRoute(path: '/page', builder: ...), // → /child/page (NOT /parent/child/page)
];
}
If you want nested prefixes, define the full path:
class ChildModule extends Module {
@override
String get routePrefix => '/parent/child';
@override
List<RouteBase> get routes => [
GoRoute(path: '/page', builder: ...), // → /parent/child/page
];
}
GoRouter Integration
Basic Router Setup
import 'package:flutter/material.dart';
import 'package:nest_flutter/nest_flutter.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: Modular.router((router) => router),
);
}
}
Router with Configuration
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'My App',
theme: ThemeData(primarySwatch: Colors.blue),
routerConfig: Modular.router(
(router) => GoRouter(
routes: router.routes,
initialLocation: '/home',
debugLogDiagnostics: true,
redirect: _handleRedirect,
errorBuilder: _buildErrorScreen,
),
),
);
}
String? _handleRedirect(BuildContext context, GoRouterState state) {
final authService = Modular.get<AuthService>();
final isLoggedIn = authService.isAuthenticated;
final isAuthRoute = state.location.startsWith('/auth');
if (!isLoggedIn && !isAuthRoute) {
return '/auth/login';
}
if (isLoggedIn && isAuthRoute) {
return '/home';
}
return null; // No redirect
}
Widget _buildErrorScreen(BuildContext context, GoRouterState state) {
return Scaffold(
appBar: AppBar(title: Text('Error')),
body: Center(
child: Text('Error: ${state.error}'),
),
);
}
}
Router Caching
The router is cached by default to prevent recreation during hot reloads.
// Check if router is cached
if (Modular.isRouterCached) {
print('Using cached router');
}
// Get cached router (for debugging)
final cachedRouter = Modular.cachedRouter;
// Clear cache to force recreation
Modular.clearRouterCache();
// Force recreation even if cached
final router = Modular.router(
(router) => router,
forceRecreate: true,
);
Route Definition Patterns
Simple Routes
class HomeModule extends Module {
@override
List<RouteBase> get routes => [
GoRoute(
path: '/home',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/about',
builder: (context, state) => AboutScreen(),
),
];
}
Named Routes
class UserModule extends Module {
@override
String get routePrefix => '/users';
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
name: 'users',
builder: (context, state) => UserListScreen(),
),
GoRoute(
path: '/:id',
name: 'user-detail',
builder: (context, state) {
final userId = state.pathParameters['id']!;
return UserDetailScreen(userId: userId);
},
),
];
}
// Navigate using named routes
context.goNamed('user-detail', pathParameters: {'id': '123'});
Nested Routes
class ProductModule extends Module {
@override
String get routePrefix => '/products';
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
builder: (context, state) => ProductListScreen(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) {
final productId = state.pathParameters['id']!;
return ProductDetailScreen(productId: productId);
},
routes: [
GoRoute(
path: 'reviews',
builder: (context, state) {
final productId = state.pathParameters['id']!;
return ProductReviewsScreen(productId: productId);
},
),
],
),
],
),
];
}
// Results in routes:
// /products
// /products/:id
// /products/:id/reviews
Routes with Query Parameters
class SearchModule extends Module {
@override
List<RouteBase> get routes => [
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final category = state.uri.queryParameters['category'];
return SearchScreen(
query: query,
category: category,
);
},
),
];
}
// Navigate with query parameters
context.go('/search?q=flutter&category=widgets');
Routes with Custom Transitions
class AnimatedModule extends Module {
@override
List<RouteBase> get routes => [
GoRoute(
path: '/animated',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: AnimatedScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
),
),
];
}
Advanced Patterns
Authentication Guard
class AppModule extends Module {
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
redirect: (context, state) {
final authService = Modular.get<AuthService>();
return authService.isAuthenticated ? '/home' : '/auth/login';
},
),
];
}
Role-Based Access
GoRoute(
path: '/admin',
redirect: (context, state) {
final authService = Modular.get<AuthService>();
if (!authService.isAuthenticated) {
return '/auth/login';
}
if (!authService.hasRole('admin')) {
return '/unauthorized';
}
return null; // Allow access
},
builder: (context, state) => AdminDashboard(),
)
Dynamic Route Loading
class DynamicModule extends Module {
@override
List<RouteBase> get routes {
final featureFlags = Modular.get<FeatureFlags>();
final routes = <RouteBase>[
GoRoute(path: '/home', builder: (context, state) => HomeScreen()),
];
if (featureFlags.isEnabled('beta-feature')) {
routes.add(
GoRoute(
path: '/beta',
builder: (context, state) => BetaFeatureScreen(),
),
);
}
return routes;
}
}
Navigation
Imperative Navigation
// Push new route
context.go('/users/123');
// Replace current route
context.replace('/home');
// Go back
context.pop();
// Named navigation
context.goNamed('user-detail', pathParameters: {'id': '123'});
Declarative Navigation
TextButton(
onPressed: () => context.go('/profile'),
child: Text('View Profile'),
)
Best Practices
Organize routes with modules: Keep routes close to the features they serve. Each feature module should define its own routes.
Use route prefixes for organization: Group related routes under a common prefix (e.g., /auth, /admin, /api).
Leverage router caching: The default caching behavior improves hot reload performance in development.
Clear cache when routes change: If you modify routes at runtime, call Modular.clearRouterCache() to ensure changes take effect.
Avoid duplicate module types: The same module type in multiple places will only be processed once. Design your module hierarchy to avoid unintended duplicates.
Debugging
Enable Router Diagnostics
Modular.router(
(router) => GoRouter(
routes: router.routes,
debugLogDiagnostics: true, // Enable detailed logging
),
)
Inspect Collected Routes
final appModule = AppModule();
final routes = appModule.collectAllRoutes();
print('Total routes: ${routes.length}');
for (final route in routes) {
if (route is GoRoute) {
print('Route: ${route.path}, Name: ${route.name}');
}
}
Check Router Cache
if (Modular.isRouterCached) {
print('Router is cached');
final router = Modular.cachedRouter;
print('Cached router configuration: ${router?.configuration}');
} else {
print('No router cached');
}
See Also