fill
This commit is contained in:
426
ROUTER_SETUP.md
Normal file
426
ROUTER_SETUP.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# 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
|
||||
1. **`/lib/core/router/app_router.dart`** - Main router configuration
|
||||
2. **`/lib/core/router/README.md`** - Detailed router documentation
|
||||
3. **`/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart`** - Integration examples
|
||||
|
||||
### Modified Files
|
||||
1. **`/lib/main.dart`** - Updated to use new router provider
|
||||
2. **`/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:
|
||||
```dart
|
||||
// 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:
|
||||
```dart
|
||||
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:
|
||||
```dart
|
||||
// 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
|
||||
```dart
|
||||
context.goToLogin();
|
||||
```
|
||||
|
||||
#### 2. Navigate to Warehouses
|
||||
```dart
|
||||
context.goToWarehouses();
|
||||
```
|
||||
|
||||
#### 3. Navigate to Operations with Warehouse
|
||||
```dart
|
||||
// From warehouse selection page
|
||||
void onWarehouseSelected(WarehouseEntity warehouse) {
|
||||
context.goToOperations(warehouse);
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Navigate to Products with Warehouse and Operation
|
||||
```dart
|
||||
// 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
|
||||
```dart
|
||||
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
|
||||
```dart
|
||||
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
|
||||
|
||||
1. **App Starts**
|
||||
- Router checks `SecureStorage.isAuthenticated()`
|
||||
- If no token → Redirects to `/login`
|
||||
- If token exists → Allows navigation
|
||||
|
||||
2. **User Logs In**
|
||||
```dart
|
||||
// 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
|
||||
```
|
||||
|
||||
3. **User Logs Out**
|
||||
```dart
|
||||
// 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
|
||||
```dart
|
||||
// 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:
|
||||
```dart
|
||||
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:
|
||||
```dart
|
||||
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:
|
||||
```dart
|
||||
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
|
||||
```dart
|
||||
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
|
||||
```dart
|
||||
context.goToLoginNamed();
|
||||
context.goToWarehousesNamed();
|
||||
context.goToOperationsNamed(warehouse);
|
||||
context.goToProductsNamed(warehouse: w, operationType: 'export');
|
||||
```
|
||||
|
||||
## Testing Authentication Flow
|
||||
|
||||
### Test Case 1: Fresh Install
|
||||
1. App starts → No token → Redirects to `/login`
|
||||
2. User logs in → Token saved → Redirects to `/warehouses`
|
||||
3. User selects warehouse → Navigates to `/operations`
|
||||
4. User selects operation → Navigates to `/products`
|
||||
|
||||
### Test Case 2: Logged In User
|
||||
1. App starts → Token exists → Shows `/warehouses`
|
||||
2. User navigates normally through app
|
||||
3. User logs out → Token cleared → Redirects to `/login`
|
||||
|
||||
### Test Case 3: Manual URL Entry
|
||||
1. User tries to access `/products` directly
|
||||
2. Router checks authentication
|
||||
3. If not authenticated → Redirects to `/login`
|
||||
4. 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
|
||||
```dart
|
||||
// In LoginUseCase
|
||||
await secureStorage.saveAccessToken(user.accessToken);
|
||||
```
|
||||
|
||||
### Problem: Redirect loop between login and warehouses
|
||||
**Solution**: Verify `isAuthenticated()` logic
|
||||
```dart
|
||||
// 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
|
||||
```dart
|
||||
// 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
|
||||
```dart
|
||||
GoRouterRefreshStream(this.ref) {
|
||||
ref.listen(
|
||||
authProvider, // Must be the correct provider
|
||||
(_, __) => notifyListeners(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Implement Warehouse Provider**
|
||||
- Create warehouse state management
|
||||
- Load warehouses from API
|
||||
- Integrate with warehouse selection page
|
||||
|
||||
2. **Implement Products Provider**
|
||||
- Create products state management
|
||||
- Load products based on warehouse and operation
|
||||
- Integrate with products page
|
||||
|
||||
3. **Add Loading States**
|
||||
- Show loading indicators during navigation
|
||||
- Handle network errors gracefully
|
||||
|
||||
4. **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.
|
||||
Reference in New Issue
Block a user