Skip to content

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 configuration
features/[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 parameters

1. 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

  1. Static Constants: Always define name and path as static constants
  2. Parameter Handling: Accept parameters through constructor
  3. Optional Parameters: Use nullable types for optional parameters
  4. Documentation: Document the page purpose and parameters
  5. 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);
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},
);
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);
    });
  });
}
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

  1. Central Definition: Define all routes in lib/app/routes.dart
  2. Static Constants: Use static name and path constants in pages
  3. Type Safety: Use named routes with parameters for type safety
  4. Consistent Naming: Use consistent naming patterns for routes

Parameter Handling

  1. Required Parameters: Use non-nullable types for required parameters
  2. Optional Parameters: Use nullable types for optional parameters
  3. Validation: Validate parameters in route builders
  4. Error Handling: Handle invalid parameters gracefully
  1. Context Usage: Use GoRouter.of(context) instead of context extensions
  2. Named Routes: Prefer named routes over path-based navigation
  3. Parameter Safety: Use pathParameters for type-safe parameter passing
  4. State Management: Consider navigation state in your app's state management

Performance

  1. Lazy Loading: Use lazy loading for heavy pages
  2. Transition Optimization: Choose appropriate transitions for UX
  3. Route Caching: Leverage GoRouter's built-in route caching
  4. 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.