Network Layer Documentation
This network layer provides a comprehensive HTTP client implementation using Dio with advanced features like authentication, retry logic, error handling, and connectivity monitoring.
Features
- ✅ Configured Dio client with timeouts and base URL management
- ✅ Authentication interceptor with automatic token refresh
- ✅ Comprehensive error handling with domain-specific exceptions
- ✅ Request/response logging for debugging
- ✅ Automatic retry logic for failed requests
- ✅ Network connectivity monitoring
- ✅ Certificate pinning setup (configurable)
- ✅ File upload/download support
- ✅ Standardized API response models
Architecture
lib/core/network/
├── dio_client.dart # Main HTTP client wrapper
├── api_constants.dart # API configuration and endpoints
├── network_info.dart # Connectivity monitoring
├── interceptors/
│ ├── auth_interceptor.dart # Token management and refresh
│ ├── logging_interceptor.dart # Request/response logging
│ └── error_interceptor.dart # Error handling and mapping
├── models/
│ └── api_response.dart # Standardized response models
└── README.md # This documentation
Quick Start
1. Setup Providers
// In your app, use the pre-configured providers
final dioClient = ref.watch(dioClientProvider);
// Or manually create
final dioClient = DioClient(
networkInfo: NetworkInfoImpl(Connectivity()),
secureStorage: FlutterSecureStorage(),
);
2. Basic HTTP Requests
// GET request
final response = await dioClient.get('/users/123');
// POST request
final response = await dioClient.post('/posts', data: {
'title': 'My Post',
'content': 'Post content'
});
// PUT request
final response = await dioClient.put('/users/123', data: userData);
// DELETE request
final response = await dioClient.delete('/posts/456');
3. File Operations
// Upload file
final response = await dioClient.uploadFile(
'/upload',
File('/path/to/file.jpg'),
filename: 'avatar.jpg',
);
// Download file
await dioClient.downloadFile(
'/files/document.pdf',
'/local/path/document.pdf',
);
4. Authentication
// Store tokens after login
await dioClient.authInterceptor.storeTokens(
accessToken: 'your-access-token',
refreshToken: 'your-refresh-token',
expiresIn: 3600, // 1 hour
);
// Check authentication status
final isAuth = await dioClient.authInterceptor.isAuthenticated();
// Logout (clears tokens)
await dioClient.authInterceptor.logout();
5. Network Connectivity
// Check current connectivity
final isConnected = await dioClient.isConnected;
// Listen to connectivity changes
dioClient.connectionStream.listen((isConnected) {
if (isConnected) {
print('Connected to internet');
} else {
print('No internet connection');
}
});
Configuration
API Constants (api_constants.dart)
class ApiConstants {
// Environment URLs
static const String baseUrlDev = 'https://api-dev.example.com';
static const String baseUrlProd = 'https://api.example.com';
// Timeouts
static const int connectTimeout = 30000;
static const int receiveTimeout = 30000;
// Retry configuration
static const int maxRetries = 3;
static const Duration retryDelay = Duration(seconds: 1);
// Endpoints
static const String loginEndpoint = '/auth/login';
static const String userEndpoint = '/user';
}
Environment Switching
// Update base URL at runtime
dioClient.updateBaseUrl('https://api-staging.example.com');
// Add custom headers
dioClient.addHeader('X-Custom-Header', 'value');
// Remove headers
dioClient.removeHeader('X-Custom-Header');
Error Handling
The network layer provides comprehensive error handling with domain-specific exceptions:
try {
final response = await dioClient.get('/api/data');
// Handle success
} on DioException catch (e) {
final failure = e.networkFailure;
failure.when(
serverError: (statusCode, message, errors) {
// Handle server errors (5xx)
},
networkError: (message) {
// Handle network connectivity issues
},
timeoutError: (message) {
// Handle timeout errors
},
unauthorizedError: (message) {
// Handle authentication errors (401)
},
forbiddenError: (message) {
// Handle authorization errors (403)
},
notFoundError: (message) {
// Handle not found errors (404)
},
validationError: (message, errors) {
// Handle validation errors (422)
},
unknownError: (message) {
// Handle unknown errors
},
);
}
Error Types
- ServerError: HTTP 5xx errors from the server
- NetworkConnectionError: Network connectivity issues
- TimeoutError: Request/response timeouts
- UnauthorizedError: HTTP 401 authentication failures
- ForbiddenError: HTTP 403 authorization failures
- NotFoundError: HTTP 404 resource not found
- ValidationError: HTTP 422 validation failures with field details
- UnknownError: Any other unexpected errors
Authentication Flow
The authentication interceptor automatically handles:
- Adding tokens to requests (Authorization header)
- Token refresh when access token expires
- Retry failed requests after token refresh
- Token storage in secure storage
- Automatic logout when refresh fails
Token Storage
Tokens are securely stored using flutter_secure_storage:
access_token: Current access tokenrefresh_token: Refresh token for getting new access tokenstoken_expiry: Token expiration timestamp
Automatic Refresh
When a request fails with 401 Unauthorized:
- Interceptor checks if refresh token exists
- Makes refresh request to
/auth/refresh - Stores new tokens if successful
- Retries original request with new token
- If refresh fails, clears all tokens
Retry Logic
Requests are automatically retried for:
- Connection timeouts
- Server errors (5xx)
- Network connectivity issues
Configuration:
- Maximum retries: 3
- Delay between retries: Progressive (1s, 2s, 3s)
- Only retries on recoverable errors
Logging
Request/response logging is automatically handled by the logging interceptor:
Log Output Example
🚀 REQUEST: GET https://api.example.com/api/v1/users/123
📋 Headers: {"Authorization": "***HIDDEN***", "Content-Type": "application/json"}
✅ RESPONSE: GET https://api.example.com/api/v1/users/123 [200] (245ms)
📥 Response Body: {"id": 123, "name": "John Doe"}
Log Features
- Request/response timing
- Sensitive header sanitization (Authorization, API keys, etc.)
- Body truncation for large responses
- Error stack traces in debug mode
- Configurable log levels
Controlling Logging
// Disable logging
dioClient.setLoggingEnabled(false);
// Create client with custom logging
final loggingInterceptor = LoggingInterceptor(
enabled: true,
logRequestBody: true,
logResponseBody: false, // Disable response body logging
maxBodyLength: 1000, // Limit body length
);
Network Monitoring
The network info service provides detailed connectivity information:
final networkInfo = NetworkInfoImpl(Connectivity());
// Simple connectivity check
final isConnected = await networkInfo.isConnected;
// Detailed connection info
final details = await networkInfo.getConnectionDetails();
print(details.connectionDescription); // "Connected via WiFi"
// Connection type checks
final isWiFi = await networkInfo.isConnectedToWiFi;
final isMobile = await networkInfo.isConnectedToMobile;
API Response Models
Basic API Response
class ApiResponse<T> {
final bool success;
final String message;
final T? data;
final List<String>? errors;
final Map<String, dynamic>? meta;
// Factory constructors
factory ApiResponse.success({required T data});
factory ApiResponse.error({required String message});
}
Usage with Services
class UserService {
Future<User> getUser(String id) async {
final response = await dioClient.get('/users/$id');
return handleApiResponse(
response,
(data) => User.fromJson(data),
);
}
T handleApiResponse<T>(Response response, T Function(dynamic) fromJson) {
if (response.statusCode == 200) {
return fromJson(response.data);
}
throw Exception('Request failed');
}
}
Best Practices
1. Use Providers for Dependency Injection
final userServiceProvider = Provider((ref) {
final dioClient = ref.watch(dioClientProvider);
return UserService(dioClient);
});
2. Create Service Classes
class UserService extends BaseApiService {
UserService(super.dioClient);
Future<User> getUser(String id) => executeRequest(
() => dioClient.get('/users/$id'),
User.fromJson,
);
}
3. Handle Errors Gracefully
try {
final user = await userService.getUser('123');
// Handle success
} catch (e) {
// Show user-friendly error message
showErrorSnackBar(context, e.toString());
}
4. Use Network Status
Widget build(BuildContext context) {
final networkStatus = ref.watch(networkConnectivityProvider);
return networkStatus.when(
data: (isConnected) => isConnected
? MainContent()
: OfflineWidget(),
loading: () => LoadingWidget(),
error: (_, __) => ErrorWidget(),
);
}
5. Configure for Different Environments
class ApiEnvironment {
static String get baseUrl {
if (kDebugMode) return ApiConstants.baseUrlDev;
if (kProfileMode) return ApiConstants.baseUrlStaging;
return ApiConstants.baseUrlProd;
}
}
Testing
Mock Network Responses
class MockDioClient extends DioClient {
@override
Future<Response<T>> get<T>(String path, {options, cancelToken, queryParameters}) async {
// Return mock response
return Response(
data: {'id': 1, 'name': 'Test User'},
statusCode: 200,
requestOptions: RequestOptions(path: path),
);
}
}
Test Network Info
class MockNetworkInfo implements NetworkInfo {
final bool _isConnected;
MockNetworkInfo({required bool isConnected}) : _isConnected = isConnected;
@override
Future<bool> get isConnected => Future.value(_isConnected);
}
Security Considerations
1. Certificate Pinning
// Enable in production
class ApiConstants {
static const bool enableCertificatePinning = true;
static const List<String> certificateHashes = [
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',
];
}
2. Secure Token Storage
Tokens are automatically stored in secure storage with platform-specific encryption.
3. Request Sanitization
Sensitive headers are automatically sanitized in logs to prevent token leakage.
Troubleshooting
Common Issues
-
Connection Timeouts
- Increase timeout values in
ApiConstants - Check network connectivity
- Verify server availability
- Increase timeout values in
-
Authentication Failures
- Ensure tokens are correctly stored
- Verify refresh endpoint configuration
- Check token expiration handling
-
Certificate Errors
- Disable certificate pinning in development
- Add proper certificate hashes for production
- Check server SSL configuration
Debug Mode
Enable detailed logging to troubleshoot issues:
final dioClient = DioClient(
networkInfo: networkInfo,
secureStorage: secureStorage,
);
// Enable detailed logging
dioClient.setLoggingEnabled(true);
Migration Guide
When migrating from basic Dio to this network layer:
- Replace
Dio()instances withDioClient - Update error handling to use
NetworkFailuretypes - Use providers for dependency injection
- Migrate to service classes extending
BaseApiService - Update authentication flow to use interceptor methods
This network layer provides a solid foundation for any Flutter app requiring robust HTTP communication with proper error handling, authentication, and network monitoring.