Files
minhthu/ROUTER_SETUP.md
2025-10-28 00:09:46 +07:00

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

  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:

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

  1. App Starts

    • Router checks SecureStorage.isAuthenticated()
    • If no token → Redirects to /login
    • If token exists → Allows navigation
  2. 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
    
  3. 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

  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

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

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