Overview
Softbee uses GoRouter for declarative routing with deep linking support, authentication guards, and type-safe navigation. The routing system is tightly integrated with Riverpod for auth-based redirects.
Router Setup
The main router is configured as a Riverpod provider:
lib/core/router/app_router.dart
final appRouterProvider = Provider < GoRouter >((ref) {
final notifier = ref. watch (routerNotifierProvider);
return GoRouter (
refreshListenable : notifier,
initialLocation : kIsWeb ? AppRoutes .landingRoute : AppRoutes .loginRoute,
routes : [
// Route definitions
],
redirect : (context, state) {
// Auth-based redirects
},
errorBuilder : (context, state) => const NotFoundPage (),
);
});
The router automatically refreshes when authentication state changes via RouterNotifier.
Route Definitions
All route paths are defined as constants:
lib/core/router/app_routes.dart
abstract class AppRoutes {
// Public routes
static const String landingRoute = '/' ;
static const String loginRoute = '/login' ;
static const String registerRoute = '/register' ;
static const String resetPasswordRoute = '/reset-password/:token' ;
// Protected routes
static const String dashboardRoute = '/dashboard' ;
static const String userProfileRoute = '/profile' ;
// Apiary-specific routes
static const String apiaryDashboardRoute = '/apiary-dashboard/:apiaryId' ;
static const String monitoringOverviewRoute =
'/apiary-dashboard/:apiaryId/monitoring' ;
static const String beehiveManagementRoute =
'/apiary-dashboard/:apiaryId/hives' ;
static const String inventoryRoute = '/apiary-dashboard/:apiaryId/inventory' ;
static const String reportsRoute = '/apiary-dashboard/:apiaryId/reports' ;
static const String historyRoute = '/apiary-dashboard/:apiaryId/history' ;
static const String apiarySettingsRoute =
'/apiary-dashboard/:apiaryId/settings' ;
}
Always use route constants from AppRoutes - never hardcode paths in navigation code.
Router Notifier
The RouterNotifier listens to auth state changes and triggers router refresh:
lib/core/router/app_router.dart
class RouterNotifier extends ChangeNotifier {
final Ref _ref;
RouterNotifier ( this ._ref) {
_ref. listen < AuthState >(
authControllerProvider,
(_, __) => notifyListeners (),
);
}
}
final routerNotifierProvider = Provider < RouterNotifier >((ref) {
return RouterNotifier (ref);
});
Route Configuration
Simple Routes
GoRoute (
path : AppRoutes .landingRoute,
builder : (context, state) => const LandingPage (),
),
GoRoute (
path : AppRoutes .dashboardRoute,
builder : (context, state) => const MenuScreen (),
),
Routes with Path Parameters
GoRoute (
name : AppRoutes .apiaryDashboardRoute,
path : AppRoutes .apiaryDashboardRoute,
builder : (context, state) {
final apiaryId = state.pathParameters[ 'apiaryId' ] as String ;
final apiaryName = state.uri.queryParameters[ 'apiaryName' ];
final apiaryLocation = state.uri.queryParameters[ 'apiaryLocation' ];
return ApiaryDashboardMenu (
apiaryId : apiaryId,
apiaryName : apiaryName ?? 'Apiario Desconocido' ,
apiaryLocation : apiaryLocation,
);
},
)
Path Parameters vs Query Parameters
Path parameters (:apiaryId): Required parts of the URL path
Query parameters (?apiaryName=value): Optional data passed in URL
Nested Routes
GoRoute (
path : AppRoutes .apiaryDashboardRoute,
builder : (context, state) => ApiaryDashboardMenu (...),
routes : [
GoRoute (
path : 'monitoring' ,
name : AppRoutes .monitoringOverviewRoute,
builder : (context, state) {
final apiaryId = state.pathParameters[ 'apiaryId' ] as String ;
return MonitoringOverviewPage (apiaryId : apiaryId);
},
routes : [
GoRoute (
path : 'questions' ,
name : AppRoutes .questionsManagementRoute,
builder : (context, state) {
final apiaryId = state.pathParameters[ 'apiaryId' ] ! ;
return QuestionsManagementScreen (apiaryId : apiaryId);
},
),
],
),
GoRoute (
path : 'inventory' ,
name : AppRoutes .inventoryRoute,
builder : (context, state) {
final apiaryId = state.pathParameters[ 'apiaryId' ] as String ;
return InventoryManagementPage (apiaryId : apiaryId);
},
),
GoRoute (
path : 'settings' ,
name : AppRoutes .apiarySettingsRoute,
builder : (context, state) {
final apiaryId = state.pathParameters[ 'apiaryId' ] as String ;
return ApiarySettingsPage (apiaryId : apiaryId);
},
),
],
)
Authentication Guards
The router implements global authentication logic via the redirect callback:
lib/core/router/app_router.dart
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;
// Wait for auth check to complete
if (authState.isAuthenticating) {
return null ; // Don't redirect while checking auth
}
// Redirect to login if not authenticated and accessing protected route
if ( ! isLoggedIn && ! isAuthRoute && ! isLandingRoute) {
return AppRoutes .loginRoute;
}
// Redirect to dashboard if authenticated and on auth/landing page
if (isLoggedIn && (isAuthRoute || isLandingRoute)) {
return AppRoutes .dashboardRoute;
}
return null ; // No redirect needed
},
The redirect logic prevents unauthenticated access to protected routes and avoids redirect loops during auth state initialization.
Navigation Patterns
Basic Navigation
Declarative (Recommended)
Imperative
// Navigate to a route
context. go ( AppRoutes .dashboardRoute);
// Navigate and replace current route
context. go ( AppRoutes .loginRoute);
Navigation with Parameters
// Path parameters
context. go ( '/apiary-dashboard/123' );
// Named routes with parameters
context. goNamed (
AppRoutes .apiaryDashboardRoute,
pathParameters : { 'apiaryId' : '123' },
queryParameters : {
'apiaryName' : 'My Apiary' ,
'apiaryLocation' : 'Farm Location' ,
},
);
Programmatic Navigation in Controllers
class ApiariesController extends StateNotifier < ApiariesState > {
// ...
Future < void > selectApiary ( String apiaryId, BuildContext context) async {
context. goNamed (
AppRoutes .apiaryDashboardRoute,
pathParameters : { 'apiaryId' : apiaryId},
);
}
}
Avoid passing BuildContext to controllers. Instead, handle navigation in the widget layer after state changes.
Deep Linking
GoRouter automatically handles deep links and web URLs:
// These URLs all work automatically:
// https://softbee.app/
// https://softbee.app/login
// https://softbee.app/apiary-dashboard/123
// https://softbee.app/apiary-dashboard/123/monitoring
initialLocation : kIsWeb
? AppRoutes .landingRoute
: AppRoutes .loginRoute,
Web : Start at landing page
Mobile : Start at login page
Error Handling
Custom 404 page for invalid routes:
errorBuilder : (context, state) => const NotFoundPage (),
Navigation Best Practices
Prefer named routes with goNamed() for type safety and easier refactoring: // Good
context. goNamed (
AppRoutes .apiaryDashboardRoute,
pathParameters : { 'apiaryId' : id},
);
// Avoid
context. go ( '/apiary-dashboard/ $ id ' );
Always define routes in AppRoutes class - never use string literals in navigation code.
Handle Navigation in Widgets
Auth State Synchronization
The router automatically syncs with auth state via RouterNotifier. No manual intervention needed.
Testing Navigation
testWidgets ( 'Redirects to login when not authenticated' , (tester) async {
final container = ProviderContainer (
overrides : [
authControllerProvider. overrideWith (
(ref) => MockAuthController ().. setUnauthenticated (),
),
],
);
await tester. pumpWidget (
UncontrolledProviderScope (
container : container,
child : MaterialApp . router (
routerConfig : container. read (appRouterProvider),
),
),
);
await tester. pumpAndSettle ();
// Verify redirected to login
expect (find. byType ( LoginPage ), findsOneWidget);
});
Route Structure Visualization
Common Navigation Examples
Login to Dashboard
Select Apiary
Navigate to Nested Route
Logout
// After successful login
context. go ( AppRoutes .dashboardRoute);
Next Steps
State Management Learn how state drives navigation
Networking Understand API integration