This commit is contained in:
2025-10-28 00:09:46 +07:00
parent 9ebe7c2919
commit de49f564b1
110 changed files with 15392 additions and 3996 deletions

View File

@@ -0,0 +1,156 @@
# GoRouter Quick Reference
## Import
```dart
import 'package:minhthu/core/router/app_router.dart';
```
## Navigation Commands
### Basic Navigation
```dart
// Login page
context.goToLogin();
// Warehouses list
context.goToWarehouses();
// Operations (requires warehouse)
context.goToOperations(warehouse);
// Products (requires warehouse and operation type)
context.goToProducts(
warehouse: warehouse,
operationType: 'import', // or 'export'
);
// Go back
context.goBack();
```
### Named Routes (Alternative)
```dart
context.goToLoginNamed();
context.goToWarehousesNamed();
context.goToOperationsNamed(warehouse);
context.goToProductsNamed(
warehouse: warehouse,
operationType: 'export',
);
```
## Common Usage Patterns
### Warehouse Selection → Operations
```dart
onTap: () {
context.goToOperations(warehouse);
}
```
### Operation Selection → Products
```dart
// Import
onTap: () {
context.goToProducts(
warehouse: warehouse,
operationType: 'import',
);
}
// Export
onTap: () {
context.goToProducts(
warehouse: warehouse,
operationType: 'export',
);
}
```
### Logout
```dart
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
await ref.read(authProvider.notifier).logout();
// Router auto-redirects to /login
},
)
```
## Route Paths
| Path | Name | Description |
|------|------|-------------|
| `/login` | `login` | Login page |
| `/warehouses` | `warehouses` | Warehouse list (protected) |
| `/operations` | `operations` | Operation selection (protected) |
| `/products` | `products` | Product list (protected) |
## Authentication
### Check Status
```dart
final isAuth = await SecureStorage().isAuthenticated();
```
### Auto-Redirect Rules
- Not authenticated → `/login`
- Authenticated on `/login``/warehouses`
- Missing parameters → Previous valid page
## Error Handling
### Missing Parameters
```dart
// Automatically redirected to safe page
// Error screen shown briefly
```
### Page Not Found
```dart
// Custom 404 page shown
// Can navigate back to login
```
## Complete Example
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:minhthu/core/router/app_router.dart';
class WarehouseCard extends ConsumerWidget {
final WarehouseEntity warehouse;
const WarehouseCard({required this.warehouse});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
child: ListTile(
title: Text(warehouse.name),
subtitle: Text(warehouse.code),
trailing: Icon(Icons.arrow_forward),
onTap: () {
// Navigate to operations
context.goToOperations(warehouse);
},
),
);
}
}
```
## Tips
1. **Use extension methods** - They provide type safety and auto-completion
2. **Let router handle auth** - Don't manually check authentication in pages
3. **Validate early** - Router validates parameters automatically
4. **Use named routes** - For better route management in large apps
## See Also
- Full documentation: `/lib/core/router/README.md`
- Setup guide: `/ROUTER_SETUP.md`
- Examples: `/lib/features/warehouse/presentation/pages/warehouse_selection_page_example.dart`

382
lib/core/router/README.md Normal file
View File

@@ -0,0 +1,382 @@
# 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

View File

@@ -0,0 +1,360 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/auth/presentation/pages/login_page.dart';
import '../../features/auth/di/auth_dependency_injection.dart';
import '../../features/warehouse/presentation/pages/warehouse_selection_page.dart';
import '../../features/operation/presentation/pages/operation_selection_page.dart';
import '../../features/products/presentation/pages/products_page.dart';
import '../../features/warehouse/domain/entities/warehouse_entity.dart';
import '../storage/secure_storage.dart';
/// Application router configuration using GoRouter
///
/// Implements authentication-based redirect logic:
/// - Unauthenticated users are redirected to /login
/// - Authenticated users on /login are redirected to /warehouses
/// - Proper parameter passing between routes
///
/// App Flow: Login → Warehouses → Operations → Products
class AppRouter {
final Ref ref;
final SecureStorage secureStorage;
AppRouter({
required this.ref,
required this.secureStorage,
});
late final GoRouter router = GoRouter(
debugLogDiagnostics: true,
initialLocation: '/login',
refreshListenable: GoRouterRefreshStream(ref),
redirect: _handleRedirect,
routes: [
// ==================== Auth Routes ====================
/// Login Route
/// Path: /login
/// Initial route for unauthenticated users
GoRoute(
path: '/login',
name: 'login',
builder: (context, state) => const LoginPage(),
),
// ==================== Main App Routes ====================
/// Warehouse Selection Route
/// Path: /warehouses
/// Shows list of available warehouses after login
GoRoute(
path: '/warehouses',
name: 'warehouses',
builder: (context, state) => const WarehouseSelectionPage(),
),
/// Operation Selection Route
/// Path: /operations
/// Takes warehouse data as extra parameter
/// Shows Import/Export operation options for selected warehouse
GoRoute(
path: '/operations',
name: 'operations',
builder: (context, state) {
final warehouse = state.extra as WarehouseEntity?;
if (warehouse == null) {
// If no warehouse data, redirect to warehouses
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Warehouse data is required',
);
}
return OperationSelectionPage(warehouse: warehouse);
},
),
/// Products List Route
/// Path: /products
/// Takes warehouse, warehouseName, and operationType as extra parameter
/// Shows products for selected warehouse and operation
GoRoute(
path: '/products',
name: 'products',
builder: (context, state) {
final params = state.extra as Map<String, dynamic>?;
if (params == null) {
// If no params, redirect to warehouses
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Product parameters are required',
);
}
// Extract required parameters
final warehouse = params['warehouse'] as WarehouseEntity?;
final warehouseName = params['warehouseName'] as String?;
final operationType = params['operationType'] as String?;
// Validate parameters
if (warehouse == null || warehouseName == null || operationType == null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
context.go('/warehouses');
});
return const _ErrorScreen(
message: 'Invalid product parameters',
);
}
return ProductsPage(
warehouseId: warehouse.id,
warehouseName: warehouseName,
operationType: operationType,
);
},
),
],
// ==================== Error Handling ====================
errorBuilder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Page Not Found'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Page Not Found',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'The page "${state.uri.path}" does not exist.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/login'),
child: const Text('Go to Login'),
),
],
),
),
);
},
);
/// Handle global redirect logic based on authentication status
///
/// Redirect rules:
/// 1. Check authentication status using SecureStorage
/// 2. If not authenticated and not on login page → redirect to /login
/// 3. If authenticated and on login page → redirect to /warehouses
/// 4. Otherwise, allow navigation
Future<String?> _handleRedirect(
BuildContext context,
GoRouterState state,
) async {
try {
// Check if user has access token
final isAuthenticated = await secureStorage.isAuthenticated();
final isOnLoginPage = state.matchedLocation == '/login';
// User is not authenticated
if (!isAuthenticated) {
// Allow access to login page
if (isOnLoginPage) {
return null;
}
// Redirect to login for all other pages
return '/login';
}
// User is authenticated
if (isAuthenticated) {
// Redirect away from login page to warehouses
if (isOnLoginPage) {
return '/warehouses';
}
// Allow access to all other pages
return null;
}
return null;
} catch (e) {
// On error, redirect to login for safety
debugPrint('Error in redirect: $e');
return '/login';
}
}
}
/// Provider for AppRouter
///
/// Creates and provides the GoRouter instance with dependencies
final appRouterProvider = Provider<GoRouter>((ref) {
final secureStorage = SecureStorage();
final appRouter = AppRouter(
ref: ref,
secureStorage: secureStorage,
);
return appRouter.router;
});
/// Helper class to refresh router when auth state changes
///
/// This allows GoRouter to react to authentication state changes
/// and re-evaluate redirect logic
class GoRouterRefreshStream extends ChangeNotifier {
final Ref ref;
GoRouterRefreshStream(this.ref) {
// Listen to auth state changes
// When auth state changes, notify GoRouter to re-evaluate redirects
ref.listen(
authProvider,
(_, __) => notifyListeners(),
);
}
}
/// Error screen widget for route parameter validation errors
class _ErrorScreen extends StatelessWidget {
final String message;
const _ErrorScreen({required this.message});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Error'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
'Navigation Error',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
message,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.go('/warehouses'),
child: const Text('Go to Warehouses'),
),
],
),
),
);
}
}
/// Extension methods for easier type-safe navigation
///
/// Usage:
/// ```dart
/// context.goToLogin();
/// context.goToWarehouses();
/// context.goToOperations(warehouse);
/// context.goToProducts(warehouse, 'import');
/// ```
extension AppRouterExtension on BuildContext {
/// Navigate to login page
void goToLogin() => go('/login');
/// Navigate to warehouses list
void goToWarehouses() => go('/warehouses');
/// Navigate to operation selection with warehouse data
void goToOperations(WarehouseEntity warehouse) {
go('/operations', extra: warehouse);
}
/// Navigate to products list with required parameters
///
/// [warehouse] - Selected warehouse entity
/// [operationType] - Either 'import' or 'export'
void goToProducts({
required WarehouseEntity warehouse,
required String operationType,
}) {
go(
'/products',
extra: {
'warehouse': warehouse,
'warehouseName': warehouse.name,
'operationType': operationType,
},
);
}
/// Pop current route
void goBack() => pop();
}
/// Extension for named route navigation
extension AppRouterNamedExtension on BuildContext {
/// Navigate to login page using named route
void goToLoginNamed() => goNamed('login');
/// Navigate to warehouses using named route
void goToWarehousesNamed() => goNamed('warehouses');
/// Navigate to operations using named route with warehouse
void goToOperationsNamed(WarehouseEntity warehouse) {
goNamed('operations', extra: warehouse);
}
/// Navigate to products using named route with parameters
void goToProductsNamed({
required WarehouseEntity warehouse,
required String operationType,
}) {
goNamed(
'products',
extra: {
'warehouse': warehouse,
'warehouseName': warehouse.name,
'operationType': operationType,
},
);
}
}