# API Client - Network Module A robust API client for the Flutter warehouse management app, built on top of Dio with comprehensive error handling, authentication management, and request/response logging. ## Features - **Automatic Token Management**: Automatically injects Bearer tokens from secure storage - **401 Error Handling**: Automatically clears tokens and triggers logout on unauthorized access - **Request/Response Logging**: Comprehensive logging for debugging with sensitive data redaction - **Error Transformation**: Converts Dio exceptions to custom app exceptions - **Timeout Configuration**: Configurable connection, receive, and send timeouts (30 seconds) - **Secure Storage Integration**: Uses flutter_secure_storage for token management - **Environment Support**: Easy base URL switching for different environments ## Files - `api_client.dart` - Main API client implementation - `api_response.dart` - Generic API response wrapper matching backend format - `api_client_example.dart` - Comprehensive usage examples - `README.md` - This documentation ## Installation The API client requires the following dependencies (already added to `pubspec.yaml`): ```yaml dependencies: dio: ^5.3.2 flutter_secure_storage: ^9.0.0 ``` ## Quick Start ### 1. Initialize API Client ```dart import 'package:minhthu/core/core.dart'; // Create secure storage instance final secureStorage = SecureStorage(); // Create API client with unauthorized callback final apiClient = ApiClient( secureStorage, onUnauthorized: () { // Navigate to login screen context.go('/login'); }, ); ``` ### 2. Make API Requests #### GET Request ```dart final response = await apiClient.get( '/warehouses', queryParameters: {'limit': 10}, ); ``` #### POST Request ```dart final response = await apiClient.post( '/auth/login', data: { 'username': 'user@example.com', 'password': 'password123', }, ); ``` #### PUT Request ```dart final response = await apiClient.put( '/products/123', data: {'name': 'Updated Name'}, ); ``` #### DELETE Request ```dart final response = await apiClient.delete('/products/123'); ``` ## API Response Format All API responses follow this standard format from the backend: ```dart { "Value": {...}, // The actual data "IsSuccess": true, // Success flag "IsFailure": false, // Failure flag "Errors": [], // List of error messages "ErrorCodes": [] // List of error codes } ``` Use the `ApiResponse` class to parse responses: ```dart final apiResponse = ApiResponse.fromJson( response.data, (json) => User.fromJson(json), // Parse the Value field ); if (apiResponse.isSuccess && apiResponse.value != null) { final user = apiResponse.value; print('Success: ${user.username}'); } else { print('Error: ${apiResponse.getErrorMessage()}'); } ``` ## Authentication Flow ### Login ```dart // 1. Login via API final response = await apiClient.post('/auth/login', data: credentials); // 2. Parse response final apiResponse = ApiResponse.fromJson(response.data, (json) => User.fromJson(json)); // 3. Save tokens (done by LoginUseCase) if (apiResponse.isSuccess) { final user = apiResponse.value!; await secureStorage.saveAccessToken(user.accessToken); await secureStorage.saveRefreshToken(user.refreshToken); } // 4. Subsequent requests automatically include Bearer token ``` ### Automatic Token Injection The API client automatically adds the Bearer token to all requests: ```dart // You just make the request final response = await apiClient.get('/warehouses'); // The interceptor automatically adds: // Authorization: Bearer ``` ### 401 Error Handling When a 401 Unauthorized error occurs: 1. Error is logged 2. All tokens are cleared from secure storage 3. `onUnauthorized` callback is triggered 4. App can navigate to login screen ```dart // This is handled automatically - no manual intervention needed // Just provide the callback when creating the client: final apiClient = ApiClient( secureStorage, onUnauthorized: () { // This will be called on 401 errors context.go('/login'); }, ); ``` ## Error Handling The API client transforms Dio exceptions into custom app exceptions: ```dart try { final response = await apiClient.get('/products'); } on NetworkException catch (e) { // Handle network errors (timeout, no internet, etc.) print('Network error: ${e.message}'); } on ServerException catch (e) { // Handle server errors (4xx, 5xx) print('Server error: ${e.message}'); if (e.code == '401') { // Unauthorized - already handled by interceptor } } catch (e) { // Handle unknown errors print('Unknown error: $e'); } ``` ### Error Types - `NetworkException`: Connection timeouts, no internet, certificate errors - `ServerException`: HTTP errors (400-599) with specific error codes - 401: Unauthorized (automatically handled) - 403: Forbidden - 404: Not Found - 422: Validation Error - 429: Rate Limited - 500+: Server Errors ## Logging The API client provides comprehensive logging for debugging: ### Request Logging ``` REQUEST[GET] => https://api.example.com/warehouses Headers: {Authorization: ***REDACTED***, Content-Type: application/json} Query Params: {limit: 10} Body: {...} ``` ### Response Logging ``` RESPONSE[200] => https://api.example.com/warehouses Data: {...} ``` ### Error Logging ``` ERROR[401] => https://api.example.com/warehouses Error Data: {Errors: [Unauthorized access], ErrorCodes: [AUTH_001]} ``` ### Security All sensitive headers (Authorization, api-key, token) are automatically redacted in logs: ```dart // Logged as: Headers: {Authorization: ***REDACTED***, Content-Type: application/json} ``` ## Configuration ### Timeout Settings Configure timeouts in `lib/core/constants/app_constants.dart`: ```dart static const int connectionTimeout = 30000; // 30 seconds static const int receiveTimeout = 30000; // 30 seconds static const int sendTimeout = 30000; // 30 seconds ``` ### Base URL Configure base URL in `lib/core/constants/app_constants.dart`: ```dart static const String apiBaseUrl = 'https://api.example.com'; ``` Or update dynamically: ```dart // For different environments apiClient.updateBaseUrl('https://dev-api.example.com'); // Development apiClient.updateBaseUrl('https://staging-api.example.com'); // Staging apiClient.updateBaseUrl('https://api.example.com'); // Production ``` ## API Endpoints Define endpoints in `lib/core/constants/api_endpoints.dart`: ```dart class ApiEndpoints { static const String login = '/auth/login'; static const String warehouses = '/warehouses'; static const String products = '/products'; // Dynamic endpoints static String productById(int id) => '/products/$id'; // Query parameters helper static Map productQueryParams({ required int warehouseId, required String type, }) { return { 'warehouseId': warehouseId, 'type': type, }; } } ``` ## Utility Methods ### Test Connection ```dart final isConnected = await apiClient.testConnection(); if (!isConnected) { print('Cannot connect to API'); } ``` ### Check Authentication ```dart final isAuthenticated = await apiClient.isAuthenticated(); if (!isAuthenticated) { // Navigate to login } ``` ### Get Current Token ```dart final token = await apiClient.getAccessToken(); if (token != null) { print('Token exists'); } ``` ### Clear Authentication ```dart // Logout - clears all tokens await apiClient.clearAuth(); ``` ## Integration with Repository Pattern The API client is designed to work with the repository pattern: ```dart // Remote Data Source class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource { final ApiClient apiClient; WarehouseRemoteDataSourceImpl(this.apiClient); @override Future> getWarehouses() async { final response = await apiClient.get(ApiEndpoints.warehouses); final apiResponse = ApiResponse.fromJson( response.data, (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(), ); if (apiResponse.isSuccess && apiResponse.value != null) { return apiResponse.value!; } else { throw ServerException(apiResponse.getErrorMessage()); } } } ``` ## Dependency Injection Register the API client with GetIt: ```dart final getIt = GetIt.instance; // Register SecureStorage getIt.registerLazySingleton(() => SecureStorage()); // Register ApiClient getIt.registerLazySingleton( () => ApiClient( getIt(), onUnauthorized: () { // Handle unauthorized access }, ), ); ``` ## Best Practices 1. **Always use ApiResponse**: Parse all responses using the `ApiResponse` wrapper 2. **Handle errors gracefully**: Catch specific exception types for better error handling 3. **Use endpoints constants**: Define all endpoints in `api_endpoints.dart` 4. **Don't expose Dio**: Use the provided methods (get, post, put, delete) instead of accessing `dio` directly 5. **Test connection**: Use `testConnection()` before critical operations 6. **Log appropriately**: The client logs automatically, but you can add app-level logs too ## Testing Mock the API client in tests: ```dart class MockApiClient extends Mock implements ApiClient {} void main() { late MockApiClient mockApiClient; setUp(() { mockApiClient = MockApiClient(); }); test('should get warehouses', () async { // Arrange when(mockApiClient.get(any)) .thenAnswer((_) async => Response( data: {'Value': [], 'IsSuccess': true}, statusCode: 200, requestOptions: RequestOptions(path: '/warehouses'), )); // Act & Assert final response = await mockApiClient.get('/warehouses'); expect(response.statusCode, 200); }); } ``` ## Troubleshooting ### Token not being added to requests - Ensure token is saved in secure storage - Check if token is expired - Verify `getAccessToken()` returns a value ### 401 errors not triggering logout - Verify `onUnauthorized` callback is set - Check error interceptor logs - Ensure secure storage is properly initialized ### Connection timeouts - Check network connectivity - Verify base URL is correct - Increase timeout values if needed ### Logging not appearing - Use Flutter DevTools or console - Check log level settings - Ensure developer.log is not filtered ## Examples See `api_client_example.dart` for comprehensive usage examples including: - Login flow - GET/POST/PUT/DELETE requests - Error handling - Custom options - Request cancellation - Environment switching ## Support For issues or questions: 1. Check this documentation 2. Review `api_client_example.dart` 3. Check Flutter DevTools logs 4. Review backend API documentation