Skip to main content

Overview

The Softbee app uses GoRouter for declarative routing with Flutter. The router is integrated with Riverpod for state management and includes authentication-aware navigation. Source: lib/core/router/app_router.dart

AppRouter Provider

appRouterProvider

The main router provider that creates and configures the GoRouter instance.
final appRouterProvider = Provider<GoRouter>((ref) {
  final notifier = ref.watch(routerNotifierProvider);
  return GoRouter(
    refreshListenable: notifier,
    initialLocation: kIsWeb ? AppRoutes.landingRoute : AppRoutes.loginRoute,
    routes: [...],
    redirect: (context, state) { ... },
    errorBuilder: (context, state) => const NotFoundPage(),
  );
});
refreshListenable
RouterNotifier
Notifier that triggers router refresh when auth state changes
initialLocation
String
Initial route based on platform detection (landing page for web, login for mobile)
redirect
Function
Global redirect logic for authentication-based navigation guards
errorBuilder
Function
Builder for 404/error pages (returns NotFoundPage)

RouterNotifier

A ChangeNotifier that listens to authentication state changes and triggers router rebuilds.
class RouterNotifier extends ChangeNotifier {
  final Ref _ref;
  
  RouterNotifier(this._ref) {
    _ref.listen<AuthState>(
      authControllerProvider,
      (_, __) => notifyListeners(),
    );
  }
}
Purpose: Automatically refreshes routes when user logs in/out.

Route Definitions

All route paths are defined in lib/core/router/app_routes.dart:

Main Routes

landingRoute
String
default:"/"
Landing page for web users
loginRoute
String
default:"/login"
User login page
registerRoute
String
default:"/register"
User registration page
dashboardRoute
String
default:"/dashboard"
Main dashboard showing list of apiaries
userProfileRoute
String
default:"/profile"
User profile management page
resetPasswordRoute
String
default:"/reset-password/:token"
Password reset page with token parameter

Apiary-Specific Routes

apiaryDashboardRoute
String
default:"/apiary-dashboard/:apiaryId"
Dashboard for a specific apiary
monitoringOverviewRoute
String
default:"/apiary-dashboard/:apiaryId/monitoring"
Monitoring overview for an apiary
beehiveManagementRoute
String
default:"/apiary-dashboard/:apiaryId/hives"
Beehive management page
questionsManagementRoute
String
Questions management for monitoring
inventoryRoute
String
default:"/apiary-dashboard/:apiaryId/inventory"
Inventory management page
reportsRoute
String
default:"/apiary-dashboard/:apiaryId/reports"
Reports page for an apiary
historyRoute
String
default:"/apiary-dashboard/:apiaryId/history"
History page showing apiary activity
apiarySettingsRoute
String
default:"/apiary-dashboard/:apiaryId/settings"
Settings page for an apiary
The router implements authentication-based navigation guards through the redirect callback:
redirect: (context, state) {
  final authState = ref.read(authControllerProvider);
  final isLoggedIn = authState.isAuthenticated;
  final isAuthRoute = state.matchedLocation == AppRoutes.loginRoute ||
                      state.matchedLocation == AppRoutes.registerRoute ||
                      state.matchedLocation == '/forgot-password' ||
                      state.matchedLocation.startsWith(
                        AppRoutes.resetPasswordRoute.split(':')[0],
                      );
  final isLandingRoute = state.matchedLocation == AppRoutes.landingRoute;

  // Don't redirect while checking auth status
  if (authState.isAuthenticating) {
    return null;
  }

  // Redirect to login if not authenticated and not on auth/landing route
  if (!isLoggedIn && !isAuthRoute && !isLandingRoute) {
    return AppRoutes.loginRoute;
  }

  // Redirect to dashboard if authenticated and on auth/landing route
  if (isLoggedIn && (isAuthRoute || isLandingRoute)) {
    return AppRoutes.dashboardRoute;
  }

  return null;
}

Redirect Logic

  1. While authenticating: No redirect (allows loading states)
  2. Not logged in + protected route: Redirect to login
  3. Logged in + auth route: Redirect to dashboard
  4. Otherwise: Allow navigation

Usage Examples

// Navigate to dashboard
context.go(AppRoutes.dashboardRoute);

// Navigate to apiary dashboard with parameters
context.go(
  '/apiary-dashboard/abc123?apiaryName=My Apiary&apiaryLocation=California'
);

Using Named Routes

// Navigate to monitoring overview
context.goNamed(
  AppRoutes.monitoringOverviewRoute,
  pathParameters: {'apiaryId': 'abc123'},
  queryParameters: {
    'apiaryName': 'My Apiary',
    'apiaryLocation': 'California',
  },
);

Accessing Route Parameters

builder: (context, state) {
  // Path parameters
  final apiaryId = state.pathParameters['apiaryId'] as String;
  
  // Query parameters
  final apiaryName = state.uri.queryParameters['apiaryName'];
  final apiaryLocation = state.uri.queryParameters['apiaryLocation'];
  
  return ApiaryDashboardMenu(
    apiaryId: apiaryId,
    apiaryName: apiaryName ?? 'Unknown Apiary',
    apiaryLocation: apiaryLocation,
  );
}

Nested Routes

The apiary dashboard uses nested routes for sub-pages:
GoRoute(
  path: AppRoutes.apiaryDashboardRoute,
  builder: (context, state) => ApiaryDashboardMenu(...),
  routes: [
    GoRoute(
      path: 'monitoring',
      name: AppRoutes.monitoringOverviewRoute,
      builder: (context, state) => MonitoringOverviewPage(...),
      routes: [
        GoRoute(
          path: 'beehives',
          name: AppRoutes.beehiveManagementRoute,
          builder: (context, state) => ColmenasManagementScreen(...),
        ),
        GoRoute(
          path: 'questions',
          name: AppRoutes.questionsManagementRoute,
          builder: (context, state) => QuestionsManagementScreen(...),
        ),
      ],
    ),
  ],
),

Platform Detection

The router uses platform detection for initial routing:
import 'package:flutter/foundation.dart' show kIsWeb;

initialLocation: kIsWeb 
    ? AppRoutes.landingRoute  // Web: Show landing page
    : AppRoutes.loginRoute     // Mobile: Go to login

Integration with Riverpod

The router is consumed in main.dart through the provider:
class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.watch(appRouterProvider);

    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      routerConfig: router,
    );
  }
}

See Also

Build docs developers (and LLMs) love