Skip to main content

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(),
    ),
  ];
}