12 KiB
GoRouter Navigation Setup - Complete Guide
This document explains the complete navigation setup for the warehouse management app using GoRouter with authentication-based redirects.
Files Created/Modified
New Files
/lib/core/router/app_router.dart- Main router configuration/lib/core/router/README.md- Detailed router documentation/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart- Integration examples
Modified Files
/lib/main.dart- Updated to use new router provider/lib/features/operation/presentation/pages/operation_selection_page.dart- Updated navigation
Architecture Overview
Route Structure
/login → LoginPage
/warehouses → WarehouseSelectionPage
/operations → OperationSelectionPage (requires warehouse)
/products → ProductsPage (requires warehouse + operationType)
Navigation Flow
┌─────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐
│ Login │ --> │ Warehouses │ --> │ Operations │ --> │ Products │
└─────────┘ └────────────┘ └────────────┘ └──────────┘
│ │ │ │
└─────────────────┴──────────────────┴──────────────────┘
Protected Routes
(Require Authentication via SecureStorage)
Key Features
1. Authentication-Based Redirects
- Unauthenticated users → Redirected to
/login - Authenticated users on /login → Redirected to
/warehouses - Uses
SecureStorage.isAuthenticated()to check access token
2. Type-Safe Navigation
Extension methods provide type-safe navigation:
// Type-safe with auto-completion
context.goToOperations(warehouse);
context.goToProducts(warehouse: warehouse, operationType: 'import');
// vs. error-prone manual navigation
context.go('/operations', extra: warehouse); // Less safe
3. Parameter Validation
Routes validate required parameters and redirect on error:
final warehouse = state.extra as WarehouseEntity?;
if (warehouse == null) {
// Show error and redirect to safe page
return _ErrorScreen(message: 'Warehouse data is required');
}
4. Reactive Navigation
Router automatically reacts to authentication state changes:
// Login → Router detects auth change → Redirects to /warehouses
await ref.read(authProvider.notifier).login(username, password);
// Logout → Router detects auth change → Redirects to /login
await ref.read(authProvider.notifier).logout();
Usage Guide
Basic Navigation
1. Navigate to Login
context.goToLogin();
2. Navigate to Warehouses
context.goToWarehouses();
3. Navigate to Operations with Warehouse
// From warehouse selection page
void onWarehouseSelected(WarehouseEntity warehouse) {
context.goToOperations(warehouse);
}
4. Navigate to Products with Warehouse and Operation
// From operation selection page
void onOperationSelected(WarehouseEntity warehouse, String operationType) {
context.goToProducts(
warehouse: warehouse,
operationType: operationType, // 'import' or 'export'
);
}
Complete Integration Example
Warehouse Selection Page
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:minhthu/core/router/app_router.dart';
import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart';
class WarehouseSelectionPage extends ConsumerWidget {
const WarehouseSelectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch warehouse state
final state = ref.watch(warehouseProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Select Warehouse'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
// Logout - router will auto-redirect to login
await ref.read(authProvider.notifier).logout();
},
),
],
),
body: ListView.builder(
itemCount: state.warehouses.length,
itemBuilder: (context, index) {
final warehouse = state.warehouses[index];
return ListTile(
title: Text(warehouse.name),
subtitle: Text(warehouse.code),
onTap: () {
// Type-safe navigation to operations
context.goToOperations(warehouse);
},
);
},
),
);
}
}
Operation Selection Page
import 'package:flutter/material.dart';
import 'package:minhthu/core/router/app_router.dart';
import 'package:minhthu/features/warehouse/domain/entities/warehouse_entity.dart';
class OperationSelectionPage extends StatelessWidget {
final WarehouseEntity warehouse;
const OperationSelectionPage({required this.warehouse});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(warehouse.name)),
body: Column(
children: [
ElevatedButton(
onPressed: () {
// Navigate to products with import operation
context.goToProducts(
warehouse: warehouse,
operationType: 'import',
);
},
child: const Text('Import Products'),
),
ElevatedButton(
onPressed: () {
// Navigate to products with export operation
context.goToProducts(
warehouse: warehouse,
operationType: 'export',
);
},
child: const Text('Export Products'),
),
],
),
);
}
}
Authentication Integration
How It Works
-
App Starts
- Router checks
SecureStorage.isAuthenticated() - If no token → Redirects to
/login - If token exists → Allows navigation
- Router checks
-
User Logs In
// AuthNotifier saves token and updates state await loginUseCase(request); // Saves to SecureStorage state = AuthState.authenticated(user); // GoRouterRefreshStream detects auth state change ref.listen(authProvider, (_, __) => notifyListeners()); // Router re-evaluates redirect logic // User is now authenticated → Redirects to /warehouses -
User Logs Out
// AuthNotifier clears token and resets state await secureStorage.clearAll(); state = const AuthState.initial(); // Router detects auth state change // User is no longer authenticated → Redirects to /login
SecureStorage Methods Used
// Check authentication
Future<bool> isAuthenticated() async {
final token = await getAccessToken();
return token != null && token.isNotEmpty;
}
// Save tokens (during login)
Future<void> saveAccessToken(String token);
Future<void> saveRefreshToken(String token);
// Clear tokens (during logout)
Future<void> clearAll();
Error Handling
1. Missing Route Parameters
If required parameters are missing, user sees error and gets redirected:
GoRoute(
path: '/operations',
builder: (context, state) {
final warehouse = state.extra as WarehouseEntity?;
if (warehouse == null) {
// Show error screen and redirect after frame
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Warehouse data is required',
);
}
return OperationSelectionPage(warehouse: warehouse);
},
)
2. Page Not Found
Custom 404 page with navigation back to login:
errorBuilder: (context, state) {
return Scaffold(
body: Center(
child: Column(
children: [
Icon(Icons.error_outline, size: 64),
Text('Page "${state.uri.path}" does not exist'),
ElevatedButton(
onPressed: () => context.go('/login'),
child: const Text('Go to Login'),
),
],
),
),
);
}
3. Authentication Errors
If SecureStorage throws an error, redirect to login for safety:
Future<String?> _handleRedirect(context, state) async {
try {
final isAuthenticated = await secureStorage.isAuthenticated();
// ... redirect logic
} catch (e) {
debugPrint('Error in redirect: $e');
return '/login'; // Safe fallback
}
}
Extension Methods Reference
Path-Based Navigation
context.goToLogin(); // Go to /login
context.goToWarehouses(); // Go to /warehouses
context.goToOperations(warehouse);
context.goToProducts(warehouse: w, operationType: 'import');
context.goBack(); // Pop current route
Named Route Navigation
context.goToLoginNamed();
context.goToWarehousesNamed();
context.goToOperationsNamed(warehouse);
context.goToProductsNamed(warehouse: w, operationType: 'export');
Testing Authentication Flow
Test Case 1: Fresh Install
- App starts → No token → Redirects to
/login - User logs in → Token saved → Redirects to
/warehouses - User selects warehouse → Navigates to
/operations - User selects operation → Navigates to
/products
Test Case 2: Logged In User
- App starts → Token exists → Shows
/warehouses - User navigates normally through app
- User logs out → Token cleared → Redirects to
/login
Test Case 3: Manual URL Entry
- User tries to access
/productsdirectly - Router checks authentication
- If not authenticated → Redirects to
/login - If authenticated but missing params → Redirects to
/warehouses
Troubleshooting
Problem: Stuck on login page after successful login
Solution: Check if token is being saved to SecureStorage
// In LoginUseCase
await secureStorage.saveAccessToken(user.accessToken);
Problem: Redirect loop between login and warehouses
Solution: Verify isAuthenticated() logic
// Should return true only if token exists
Future<bool> isAuthenticated() async {
final token = await getAccessToken();
return token != null && token.isNotEmpty;
}
Problem: Navigation parameters are null
Solution: Use extension methods with correct types
// Correct
context.goToOperations(warehouse);
// Wrong - may lose type information
context.go('/operations', extra: warehouse);
Problem: Router doesn't react to auth changes
Solution: Verify GoRouterRefreshStream is listening
GoRouterRefreshStream(this.ref) {
ref.listen(
authProvider, // Must be the correct provider
(_, __) => notifyListeners(),
);
}
Next Steps
-
Implement Warehouse Provider
- Create warehouse state management
- Load warehouses from API
- Integrate with warehouse selection page
-
Implement Products Provider
- Create products state management
- Load products based on warehouse and operation
- Integrate with products page
-
Add Loading States
- Show loading indicators during navigation
- Handle network errors gracefully
-
Add Analytics
- Track navigation events
- Monitor authentication flow
Related Documentation
- Router Details:
/lib/core/router/README.md - Auth Setup:
/lib/features/auth/di/auth_dependency_injection.dart - SecureStorage:
/lib/core/storage/secure_storage.dart - Examples:
/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart
Summary
The complete GoRouter setup provides:
- Authentication-based navigation with auto-redirect
- Type-safe parameter passing
- Reactive updates on auth state changes
- Proper error handling and validation
- Easy-to-use extension methods
- Integration with existing SecureStorage and Riverpod
The app flow is: Login → Warehouses → Operations → Products
All protected routes automatically redirect to login if user is not authenticated.