383 lines
9.2 KiB
Markdown
383 lines
9.2 KiB
Markdown
# App Router Documentation
|
|
|
|
Complete navigation setup for the warehouse management application using GoRouter.
|
|
|
|
## Overview
|
|
|
|
The app router implements authentication-based navigation with proper redirect logic:
|
|
- **Unauthenticated users** are redirected to `/login`
|
|
- **Authenticated users** on `/login` are redirected to `/warehouses`
|
|
- Type-safe parameter passing between routes
|
|
- Integration with SecureStorage for authentication checks
|
|
|
|
## App Flow
|
|
|
|
```
|
|
Login → Warehouses → Operations → Products
|
|
```
|
|
|
|
1. **Login**: User authenticates and token is stored
|
|
2. **Warehouses**: User selects a warehouse
|
|
3. **Operations**: User chooses Import or Export
|
|
4. **Products**: Display products based on warehouse and operation
|
|
|
|
## Routes
|
|
|
|
### `/login` - Login Page
|
|
- **Name**: `login`
|
|
- **Purpose**: User authentication
|
|
- **Parameters**: None
|
|
- **Redirect**: If authenticated → `/warehouses`
|
|
|
|
### `/warehouses` - Warehouse Selection Page
|
|
- **Name**: `warehouses`
|
|
- **Purpose**: Display list of warehouses
|
|
- **Parameters**: None
|
|
- **Protected**: Requires authentication
|
|
|
|
### `/operations` - Operation Selection Page
|
|
- **Name**: `operations`
|
|
- **Purpose**: Choose Import or Export operation
|
|
- **Parameters**:
|
|
- `extra`: `WarehouseEntity` object
|
|
- **Protected**: Requires authentication
|
|
- **Validation**: Redirects to `/warehouses` if warehouse data is missing
|
|
|
|
### `/products` - Products List Page
|
|
- **Name**: `products`
|
|
- **Purpose**: Display products for warehouse and operation
|
|
- **Parameters**:
|
|
- `extra`: `Map<String, dynamic>` containing:
|
|
- `warehouse`: `WarehouseEntity` object
|
|
- `warehouseName`: `String`
|
|
- `operationType`: `String` ('import' or 'export')
|
|
- **Protected**: Requires authentication
|
|
- **Validation**: Redirects to `/warehouses` if parameters are invalid
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Navigation
|
|
|
|
```dart
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
// Navigate to login
|
|
context.go('/login');
|
|
|
|
// Navigate to warehouses
|
|
context.go('/warehouses');
|
|
```
|
|
|
|
### Navigation with Extension Methods
|
|
|
|
```dart
|
|
import 'package:minhthu/core/router/app_router.dart';
|
|
|
|
// Navigate to login
|
|
context.goToLogin();
|
|
|
|
// Navigate to warehouses
|
|
context.goToWarehouses();
|
|
|
|
// Navigate to operations with warehouse
|
|
context.goToOperations(warehouse);
|
|
|
|
// Navigate to products with warehouse and operation type
|
|
context.goToProducts(
|
|
warehouse: warehouse,
|
|
operationType: 'import',
|
|
);
|
|
|
|
// Go back
|
|
context.goBack();
|
|
```
|
|
|
|
### Named Route Navigation
|
|
|
|
```dart
|
|
// Using named routes
|
|
context.goToLoginNamed();
|
|
context.goToWarehousesNamed();
|
|
context.goToOperationsNamed(warehouse);
|
|
context.goToProductsNamed(
|
|
warehouse: warehouse,
|
|
operationType: 'export',
|
|
);
|
|
```
|
|
|
|
## Integration with Warehouse Selection
|
|
|
|
### Example: Navigate from Warehouse to Operations
|
|
|
|
```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 WarehouseCard extends StatelessWidget {
|
|
final WarehouseEntity warehouse;
|
|
|
|
const WarehouseCard({required this.warehouse});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: ListTile(
|
|
title: Text(warehouse.name),
|
|
subtitle: Text('Code: ${warehouse.code}'),
|
|
trailing: Icon(Icons.arrow_forward),
|
|
onTap: () {
|
|
// Navigate to operations page
|
|
context.goToOperations(warehouse);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Example: Navigate from Operations to Products
|
|
|
|
```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 OperationButton extends StatelessWidget {
|
|
final WarehouseEntity warehouse;
|
|
final String operationType;
|
|
|
|
const OperationButton({
|
|
required this.warehouse,
|
|
required this.operationType,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ElevatedButton(
|
|
onPressed: () {
|
|
// Navigate to products page
|
|
context.goToProducts(
|
|
warehouse: warehouse,
|
|
operationType: operationType,
|
|
);
|
|
},
|
|
child: Text(operationType == 'import'
|
|
? 'Import Products'
|
|
: 'Export Products'),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Authentication Integration
|
|
|
|
The router automatically checks authentication status on every navigation:
|
|
|
|
```dart
|
|
// In app_router.dart
|
|
Future<String?> _handleRedirect(
|
|
BuildContext context,
|
|
GoRouterState state,
|
|
) async {
|
|
// Check if user has access token
|
|
final isAuthenticated = await secureStorage.isAuthenticated();
|
|
final isOnLoginPage = state.matchedLocation == '/login';
|
|
|
|
// Redirect logic
|
|
if (!isAuthenticated && !isOnLoginPage) {
|
|
return '/login'; // Redirect to login
|
|
}
|
|
|
|
if (isAuthenticated && isOnLoginPage) {
|
|
return '/warehouses'; // Redirect to warehouses
|
|
}
|
|
|
|
return null; // Allow navigation
|
|
}
|
|
```
|
|
|
|
### SecureStorage Integration
|
|
|
|
The router uses `SecureStorage` to check authentication:
|
|
|
|
```dart
|
|
// Check if authenticated
|
|
final isAuthenticated = await secureStorage.isAuthenticated();
|
|
|
|
// This checks if access token exists
|
|
Future<bool> isAuthenticated() async {
|
|
final token = await getAccessToken();
|
|
return token != null && token.isNotEmpty;
|
|
}
|
|
```
|
|
|
|
## Reactive Navigation
|
|
|
|
The router automatically reacts to authentication state changes:
|
|
|
|
```dart
|
|
class GoRouterRefreshStream extends ChangeNotifier {
|
|
final Ref ref;
|
|
|
|
GoRouterRefreshStream(this.ref) {
|
|
// Listen to auth state changes
|
|
ref.listen(
|
|
authProvider, // From auth_dependency_injection.dart
|
|
(_, __) => notifyListeners(),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
When authentication state changes (login/logout), the router:
|
|
1. Receives notification
|
|
2. Re-evaluates redirect logic
|
|
3. Automatically redirects to appropriate page
|
|
|
|
## Error Handling
|
|
|
|
### Missing Parameters
|
|
|
|
If route parameters are missing, the user is redirected:
|
|
|
|
```dart
|
|
GoRoute(
|
|
path: '/operations',
|
|
builder: (context, state) {
|
|
final warehouse = state.extra as WarehouseEntity?;
|
|
|
|
if (warehouse == null) {
|
|
// Show error and redirect
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go('/warehouses');
|
|
});
|
|
return const _ErrorScreen(
|
|
message: 'Warehouse data is required',
|
|
);
|
|
}
|
|
|
|
return OperationSelectionPage(warehouse: warehouse);
|
|
},
|
|
),
|
|
```
|
|
|
|
### Page Not Found
|
|
|
|
Custom 404 error page:
|
|
|
|
```dart
|
|
errorBuilder: (context, state) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Page Not Found')),
|
|
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'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
```
|
|
|
|
## Setup in main.dart
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:minhthu/core/router/app_router.dart';
|
|
import 'package:minhthu/core/theme/app_theme.dart';
|
|
|
|
void main() {
|
|
runApp(
|
|
const ProviderScope(
|
|
child: MyApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
class MyApp extends ConsumerWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// Get router from provider
|
|
final router = ref.watch(appRouterProvider);
|
|
|
|
return MaterialApp.router(
|
|
title: 'Warehouse Manager',
|
|
theme: AppTheme.lightTheme,
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use Extension Methods
|
|
Prefer extension methods for type-safe navigation:
|
|
```dart
|
|
// Good
|
|
context.goToProducts(warehouse: warehouse, operationType: 'import');
|
|
|
|
// Avoid
|
|
context.go('/products', extra: {'warehouse': warehouse, 'operationType': 'import'});
|
|
```
|
|
|
|
### 2. Validate Parameters
|
|
Always validate route parameters:
|
|
```dart
|
|
final warehouse = state.extra as WarehouseEntity?;
|
|
if (warehouse == null) {
|
|
// Handle error
|
|
}
|
|
```
|
|
|
|
### 3. Handle Async Operations
|
|
Use post-frame callbacks for navigation in builders:
|
|
```dart
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.go('/warehouses');
|
|
});
|
|
```
|
|
|
|
### 4. Logout Implementation
|
|
Clear storage and let router handle redirect:
|
|
```dart
|
|
Future<void> logout() async {
|
|
await ref.read(authProvider.notifier).logout();
|
|
// Router will automatically redirect to /login
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: Redirect loop
|
|
**Cause**: Authentication check is not working properly
|
|
**Solution**: Verify SecureStorage has access token
|
|
|
|
### Issue: Parameters are null
|
|
**Cause**: Wrong parameter passing format
|
|
**Solution**: Use extension methods with correct types
|
|
|
|
### Issue: Navigation doesn't update
|
|
**Cause**: Auth state changes not triggering refresh
|
|
**Solution**: Verify GoRouterRefreshStream is listening to authProvider
|
|
|
|
## Related Files
|
|
|
|
- `/lib/core/router/app_router.dart` - Main router configuration
|
|
- `/lib/core/storage/secure_storage.dart` - Authentication storage
|
|
- `/lib/features/auth/di/auth_dependency_injection.dart` - Auth providers
|
|
- `/lib/features/auth/presentation/pages/login_page.dart` - Login page
|
|
- `/lib/features/warehouse/presentation/pages/warehouse_selection_page.dart` - Warehouse page
|
|
- `/lib/features/operation/presentation/pages/operation_selection_page.dart` - Operation page
|
|
- `/lib/features/products/presentation/pages/products_page.dart` - Products page
|