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

458
lib/core/network/README.md Normal file
View File

@@ -0,0 +1,458 @@
# 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 <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
```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<String, dynamic> 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<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:
```dart
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:
```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