fill
This commit is contained in:
458
lib/core/network/README.md
Normal file
458
lib/core/network/README.md
Normal 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
|
||||
Reference in New Issue
Block a user