Advanced Routing Patterns
Nest Flutter provides powerful routing capabilities through the RouteMixin, enabling modular route organization with GoRouter integration.
Route Collection Mechanism
The collectAllRoutes() method recursively collects routes from a module and all its imported modules, preventing duplicate processing:
class TodoModule extends Module {
@override
List<Module> get imports => [CoreModule()];
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
builder: (context, state) => const TodoListView(),
),
GoRoute(
path: '/:id',
builder: (context, state) => TodoDetailView(
todoId: int.tryParse(state.pathParameters['id'] ?? '0') ?? 0,
),
),
];
}
The route collection mechanism uses a Set<Type> to track processed module types, preventing infinite loops in circular dependencies.
Route Prefixing
The routePrefix property allows you to namespace all routes within a module:
class TodoModule extends Module {
@override
String? get routePrefix => '/todos';
@override
List<RouteBase> get routes => [
GoRoute(path: '/', builder: (context, state) => const TodoListView()),
// Becomes /todos/
GoRoute(path: '/:id', builder: (context, state) => TodoDetailView()),
// Becomes /todos/:id
GoRoute(path: '/second', builder: (context, state) => Placeholder()),
// Becomes /todos/second
];
}
How Route Prefix Works
From packages/nest_flutter/lib/src/module.dart:48-62:
RouteBase _applyRoutePrefix(RouteBase route, String prefix) {
if (route is GoRoute) {
// Ensure prefix starts with / and doesn't end with /
final cleanPrefix = prefix.startsWith('/') ? prefix : '/$prefix';
final finalPrefix = cleanPrefix.endsWith('/')
? cleanPrefix.substring(0, cleanPrefix.length - 1)
: cleanPrefix;
// Ensure route path starts with /
final routePath = route.path.startsWith('/')
? route.path
: '/${route.path}';
// Combine prefix and path, avoiding double slashes
final newPath = routePath == '/' ? finalPrefix : '$finalPrefix$routePath';
return GoRoute(
path: newPath,
name: route.name,
builder: route.builder,
pageBuilder: route.pageBuilder,
redirect: route.redirect,
routes: route.routes,
);
}
return route;
}
The prefix normalization ensures clean paths regardless of whether you include leading/trailing slashes.
Nested Routes
Organize related routes hierarchically within a single module:
class ModuleB extends Module {
@override
String? get routePrefix => '/module-b';
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
builder: (context, state) => const MainView(),
routes: [
GoRoute(
path: 'second',
builder: (context, state) => const SecondView(),
),
GoRoute(
path: 'third/:id',
builder: (context, state) => DetailView(
id: state.pathParameters['id']!,
),
),
],
),
];
}
This creates the following route structure:
/module-b/ - MainView
/module-b/second - SecondView
/module-b/third/:id - DetailView
Multi-Module Route Organization
Routes are collected from imported modules in dependency-first order:
class AppModule extends Module {
@override
List<Module> get imports => [
AuthModule(), // Routes collected first
TodoModule(), // Then these routes
ProfileModule(), // Finally these routes
];
}
class AuthModule extends Module {
@override
String? get routePrefix => '/auth';
@override
List<RouteBase> get routes => [
GoRoute(path: '/login', builder: (context, state) => LoginView()),
GoRoute(path: '/register', builder: (context, state) => RegisterView()),
];
}
Collection Order
From packages/nest_flutter/lib/src/module.dart:27-42:
// 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);
}
Imported modules’ routes are added before the current module’s routes. Order matters if you have overlapping path patterns.
Complete Router Setup
Integrate module routes with GoRouter:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:nest_flutter/nest_flutter.dart';
class AppModule extends Module {
@override
List<Module> get imports => [
TodoModule(),
ProfileModule(),
];
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final appModule = AppModule();
await Modular.initialize(appModule);
// Collect all routes from modules
final routes = appModule.collectAllRoutes();
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeView(),
),
...routes, // Add all module routes
],
);
runApp(MyApp(router: router));
}
Best Practices
1. Use Clear Prefixes
// Good: Clear, semantic prefixes
class UserModule extends Module {
@override
String? get routePrefix => '/users';
}
// Avoid: Generic or unclear prefixes
class UserModule extends Module {
@override
String? get routePrefix => '/u';
}
class DashboardModule extends Module {
@override
String? get routePrefix => '/dashboard';
@override
List<RouteBase> get routes => [
GoRoute(
path: '/',
builder: (context, state) => const DashboardView(),
routes: [
GoRoute(path: 'analytics', builder: (context, state) => AnalyticsView()),
GoRoute(path: 'settings', builder: (context, state) => SettingsView()),
GoRoute(path: 'reports', builder: (context, state) => ReportsView()),
],
),
];
}
3. Avoid Route Conflicts
// Ensure module prefixes don't conflict
class AdminModule extends Module {
@override
String? get routePrefix => '/admin';
}
class UserModule extends Module {
@override
String? get routePrefix => '/users'; // Different prefix, no conflict
}
Use route prefixes to create clear boundaries between feature modules and prevent path conflicts.
Route Guards and Redirects
Combine route prefixes with GoRouter redirects for authentication:
class ProtectedModule extends Module {
@override
String? get routePrefix => '/app';
@override
List<RouteBase> get routes => [
GoRoute(
path: '/dashboard',
redirect: (context, state) {
final authService = Modular.get<AuthService>();
if (!authService.isAuthenticated) {
return '/login';
}
return null;
},
builder: (context, state) => const DashboardView(),
),
];
}