Files
minhthu/lib/core/router
2025-10-28 23:56:47 +07:00
..
fix
2025-10-28 23:56:47 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00

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

import 'package:go_router/go_router.dart';

// Navigate to login
context.go('/login');

// Navigate to warehouses
context.go('/warehouses');

Navigation with Extension Methods

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

// 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

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

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:

// 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:

// 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:

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:

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:

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

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:

// Good
context.goToProducts(warehouse: warehouse, operationType: 'import');

// Avoid
context.go('/products', extra: {'warehouse': warehouse, 'operationType': 'import'});

2. Validate Parameters

Always validate route parameters:

final warehouse = state.extra as WarehouseEntity?;
if (warehouse == null) {
  // Handle error
}

3. Handle Async Operations

Use post-frame callbacks for navigation in builders:

WidgetsBinding.instance.addPostFrameCallback((_) {
  context.go('/warehouses');
});

4. Logout Implementation

Clear storage and let router handle redirect:

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

  • /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