Files
minhthu/lib/core/network
2025-10-28 00:09:46 +07:00
..
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00
2025-10-28 00:09:46 +07:00

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

dependencies:
  dio: ^5.3.2
  flutter_secure_storage: ^9.0.0

Quick Start

1. Initialize API Client

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

final response = await apiClient.get(
  '/warehouses',
  queryParameters: {'limit': 10},
);

POST Request

final response = await apiClient.post(
  '/auth/login',
  data: {
    'username': 'user@example.com',
    'password': 'password123',
  },
);

PUT Request

final response = await apiClient.put(
  '/products/123',
  data: {'name': 'Updated Name'},
);

DELETE Request

final response = await apiClient.delete('/products/123');

API Response Format

All API responses follow this standard format from the backend:

{
  "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:

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

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

// You just make the request
final response = await apiClient.get('/warehouses');

// The interceptor automatically adds:
// Authorization: Bearer <token>

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

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:

// Logged as:
Headers: {Authorization: ***REDACTED***, Content-Type: application/json}

Configuration

Timeout Settings

Configure timeouts in lib/core/constants/app_constants.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:

static const String apiBaseUrl = 'https://api.example.com';

Or update dynamically:

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

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<String, dynamic> productQueryParams({
    required int warehouseId,
    required String type,
  }) {
    return {
      'warehouseId': warehouseId,
      'type': type,
    };
  }
}

Utility Methods

Test Connection

final isConnected = await apiClient.testConnection();
if (!isConnected) {
  print('Cannot connect to API');
}

Check Authentication

final isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
  // Navigate to login
}

Get Current Token

final token = await apiClient.getAccessToken();
if (token != null) {
  print('Token exists');
}

Clear Authentication

// Logout - clears all tokens
await apiClient.clearAuth();

Integration with Repository Pattern

The API client is designed to work with the repository pattern:

// Remote Data Source
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
  final ApiClient apiClient;

  WarehouseRemoteDataSourceImpl(this.apiClient);

  @override
  Future<List<Warehouse>> 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:

final getIt = GetIt.instance;

// Register SecureStorage
getIt.registerLazySingleton<SecureStorage>(() => SecureStorage());

// Register ApiClient
getIt.registerLazySingleton<ApiClient>(
  () => ApiClient(
    getIt<SecureStorage>(),
    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:

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