Skip to content

Navigation

Introduction

Navigation in Bond follows Flutter's declarative routing approach while providing additional structure, type safety, and integration with Bond's feature-based architecture. The system supports deep linking, route guards, and clean separation between navigation logic and UI components.

Why Structured Navigation

Traditional Problems

Flutter navigation often becomes unwieldy in larger applications:

// Traditional approach - scattered navigation logic
Navigator.of(context).push(
  MaterialPageRoute(
    builder: (context) => UserProfilePage(userId: userId),
  ),
);

// Hardcoded route names
Navigator.of(context).pushNamed('/user/profile', arguments: userId);

Problems: 1. Navigation logic scattered throughout widgets 2. No type safety for route parameters 3. Difficult deep linking implementation 4. No route guards for authentication 5. Hard to test navigation flows

Bond's Solution

// Bond approach - structured and type-safe
AppRouter.pushUserProfile(context, userId: userId);

// Or with route guards
AppRouter.pushProtected(context, ProfileRoute(userId: userId));

Route Definition

Route Classes

Define routes as classes for type safety:

// lib/core/routing/app_routes.dart
abstract class AppRoute {
  String get path;
  Widget build(BuildContext context);
  bool get requiresAuth => false;
}

class HomeRoute extends AppRoute {
  @override
  String get path => '/';

  @override
  Widget build(BuildContext context) => HomePage();
}

class UserProfileRoute extends AppRoute {
  final String userId;

  const UserProfileRoute({required this.userId});

  @override
  String get path => '/user/$userId';

  @override
  Widget build(BuildContext context) => UserProfilePage(userId: userId);

  @override
  bool get requiresAuth => true;
}

class PostDetailRoute extends AppRoute {
  final String postId;
  final String? commentId;

  const PostDetailRoute({
    required this.postId,
    this.commentId,
  });

  @override
  String get path => '/post/$postId${commentId != null ? '#comment-$commentId' : ''}';

  @override
  Widget build(BuildContext context) => PostDetailPage(
    postId: postId,
    highlightCommentId: commentId,
  );
}

Route Registry

// lib/core/routing/route_registry.dart
class RouteRegistry {
  static final Map<String, AppRoute Function(RouteSettings)> _routes = {
    '/': (_) => HomeRoute(),
    '/login': (_) => LoginRoute(),
    '/register': (_) => RegisterRoute(),
    '/user/:userId': (settings) {
      final userId = settings.pathParameters['userId']!;
      return UserProfileRoute(userId: userId);
    },
    '/post/:postId': (settings) {
      final postId = settings.pathParameters['postId']!;
      final commentId = settings.queryParameters['comment'];
      return PostDetailRoute(postId: postId, commentId: commentId);
    },
  };

  static AppRoute? getRoute(RouteSettings settings) {
    final routeBuilder = _routes[settings.name];
    return routeBuilder?.call(settings);
  }
}

Router Implementation

App Router

// lib/core/routing/app_router.dart
class AppRouter {
  static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  static NavigatorState get navigator => navigatorKey.currentState!;

  // Type-safe navigation methods
  static Future<void> pushHome() async {
    await navigator.pushReplacementNamed(HomeRoute().path);
  }

  static Future<void> pushUserProfile(String userId) async {
    await navigator.pushNamed(UserProfileRoute(userId: userId).path);
  }

  static Future<void> pushPostDetail(String postId, {String? commentId}) async {
    final route = PostDetailRoute(postId: postId, commentId: commentId);
    await navigator.pushNamed(route.path);
  }

  // Protected routes
  static Future<void> pushProtected(AppRoute route) async {
    final authService = GetIt.instance<AuthService>();

    if (route.requiresAuth && !await authService.isAuthenticated()) {
      await pushLogin();
      return;
    }

    await navigator.pushNamed(route.path);
  }

  // Modal routes
  static Future<T?> showModal<T>(Widget child) {
    return showModalBottomSheet<T>(
      context: navigator.context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => child,
    );
  }

  // Dialog routes
  static Future<T?> showDialog<T>(Widget child) {
    return showDialog<T>(
      context: navigator.context,
      builder: (context) => child,
    );
  }
}

Route Generator

// lib/core/routing/route_generator.dart
class RouteGenerator {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    final route = RouteRegistry.getRoute(settings);

    if (route == null) {
      return _buildErrorRoute(settings);
    }

    return PageRouteBuilder(
      settings: settings,
      pageBuilder: (context, animation, secondaryAnimation) {
        return route.build(context);
      },
      transitionsBuilder: _buildTransition,
    );
  }

  static Widget _buildTransition(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return SlideTransition(
      position: animation.drive(
        Tween(begin: Offset(1.0, 0.0), end: Offset.zero),
      ),
      child: child,
    );
  }

  static Route<dynamic> _buildErrorRoute(RouteSettings settings) {
    return MaterialPageRoute(
      builder: (context) => NotFoundPage(path: settings.name ?? '/'),
    );
  }
}

Deep Linking

URL Handling

// lib/core/routing/deep_link_handler.dart
class DeepLinkHandler {
  static Future<void> handleInitialLink() async {
    try {
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        await _processLink(initialLink);
      }
    } catch (e) {
      print('Error handling initial link: $e');
    }
  }

  static void listenForLinks() {
    getLinksStream().listen(
      (String link) => _processLink(link),
      onError: (err) => print('Deep link error: $err'),
    );
  }

  static Future<void> _processLink(String link) async {
    final uri = Uri.parse(link);

    // Handle different link types
    if (uri.pathSegments.isNotEmpty) {
      switch (uri.pathSegments.first) {
        case 'user':
          if (uri.pathSegments.length > 1) {
            await AppRouter.pushUserProfile(uri.pathSegments[1]);
          }
          break;

        case 'post':
          if (uri.pathSegments.length > 1) {
            final postId = uri.pathSegments[1];
            final commentId = uri.queryParameters['comment'];
            await AppRouter.pushPostDetail(postId, commentId: commentId);
          }
          break;

        case 'auth':
          await _handleAuthLink(uri);
          break;

        default:
          await AppRouter.pushHome();
      }
    }
  }

  static Future<void> _handleAuthLink(Uri uri) async {
    if (uri.pathSegments.length > 1) {
      switch (uri.pathSegments[1]) {
        case 'reset-password':
          final token = uri.queryParameters['token'];
          if (token != null) {
            await AppRouter.pushPasswordReset(token: token);
          }
          break;

        case 'verify-email':
          final token = uri.queryParameters['token'];
          if (token != null) {
            await AppRouter.pushEmailVerification(token: token);
          }
          break;
      }
    }
  }
}

Route Guards

Authentication Guard

// lib/core/routing/auth_guard.dart
class AuthGuard {
  final AuthService _authService;

  const AuthGuard(this._authService);

  Future<bool> canActivate(AppRoute route) async {
    if (!route.requiresAuth) return true;

    final isAuthenticated = await _authService.isAuthenticated();

    if (!isAuthenticated) {
      await AppRouter.pushLogin();
      return false;
    }

    return true;
  }
}

Permission Guard

class PermissionGuard {
  final PermissionService _permissionService;

  const PermissionGuard(this._permissionService);

  Future<bool> canActivate(AppRoute route, {List<Permission>? requiredPermissions}) async {
    if (requiredPermissions == null || requiredPermissions.isEmpty) {
      return true;
    }

    final hasPermissions = await _permissionService.hasPermissions(requiredPermissions);

    if (!hasPermissions) {
      await AppRouter.showPermissionDeniedDialog();
      return false;
    }

    return true;
  }
}

Testing Navigation

// test/core/routing/app_router_test.dart
void main() {
  group('AppRouter', () {
    late MockNavigatorObserver mockObserver;

    setUp(() {
      mockObserver = MockNavigatorObserver();
    });

    testWidgets('should navigate to user profile', (tester) async {
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: AppRouter.navigatorKey,
          navigatorObservers: [mockObserver],
          onGenerateRoute: RouteGenerator.generateRoute,
          home: HomePage(),
        ),
      );

      await AppRouter.pushUserProfile('123');
      await tester.pumpAndSettle();

      verify(mockObserver.didPush(any, any));
      expect(find.byType(UserProfilePage), findsOneWidget);
    });
  });
}

Best Practices

  • ✅ Use type-safe route classes
  • ✅ Implement route guards for protected content
  • ✅ Handle deep links consistently
  • ✅ Test navigation flows
  • ✅ Keep navigation logic out of widgets

Next Steps