This commit is contained in:
Phuoc Nguyen
2025-10-17 17:22:28 +07:00
parent 2125e85d40
commit 628c81ce13
86 changed files with 31339 additions and 1710 deletions

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

@@ -0,0 +1,449 @@
# API Integration Infrastructure - Worker App
## Overview
Comprehensive HTTP client infrastructure built with **Dio** and **Riverpod 3.0** for the Worker Flutter application. This setup provides robust API integration with authentication, caching, retry logic, error handling, and offline support.
## Architecture
```
lib/core/network/
├── dio_client.dart # Main HTTP client with Riverpod providers
├── api_interceptor.dart # Authentication, logging, and error interceptors
├── network_info.dart # Network connectivity monitoring
├── api_constants.dart # API endpoints and configuration
├── exceptions.dart # Custom exception definitions
└── failures.dart # Domain-level failure types
```
## Key Features
### 1. Dio HTTP Client (`dio_client.dart`)
**DioClient Class**
- Wrapper around Dio with full method support (GET, POST, PUT, PATCH, DELETE)
- File upload with multipart/form-data
- File download with progress tracking
- Cache management utilities
**Riverpod Providers**
- `dioProvider` - Configured Dio instance with all interceptors
- `dioClientProvider` - DioClient wrapper instance
- `cacheStoreProvider` - Hive-based cache storage
- `cacheOptionsProvider` - Cache configuration
**Configuration**
- Base URL: Configurable per environment (dev/staging/prod)
- Timeouts: 30s connection, 30s receive, 30s send
- Headers: JSON content-type, Vietnamese language by default
- Cache: 7-day max-stale, no caching on auth errors (401, 403)
### 2. Interceptors (`api_interceptor.dart`)
#### AuthInterceptor
- **Token Injection**: Automatically adds Bearer token to requests
- **Token Refresh**: Handles 401 errors with automatic token refresh
- **Public Endpoints**: Skips auth for login/OTP/register endpoints
- **Language Header**: Adds Vietnamese language preference
- **Storage**: Uses SharedPreferences for token persistence
#### LoggingInterceptor
- **Request Logging**: Method, URL, headers, body, query parameters
- **Response Logging**: Status code, response data (truncated)
- **Error Logging**: Error type, status code, error data
- **Security**: Sanitizes sensitive fields (password, OTP, tokens)
- **Format**: Beautiful formatted logs with separators
#### ErrorTransformerInterceptor
- **Dio Error Mapping**: Transforms DioException to custom exceptions
- **Status Code Handling**:
- 400 → ValidationException/BadRequestException
- 401 → UnauthorizedException/TokenExpiredException/InvalidOTPException
- 403 → ForbiddenException
- 404 → NotFoundException
- 409 → ConflictException
- 422 → ValidationException with field errors
- 429 → RateLimitException with retry-after
- 5xx → ServerException/ServiceUnavailableException
- **Connection Errors**: Timeout, NoInternet, etc.
#### RetryInterceptor
- **Exponential Backoff**: Configurable delay multiplier
- **Max Retries**: 3 attempts by default
- **Retry Conditions**:
- Connection timeout/errors
- 5xx server errors (except 501)
- 408 Request Timeout
- 429 Too Many Requests
- **Network Check**: Verifies connectivity before retrying
### 3. Network Monitoring (`network_info.dart`)
**NetworkInfo Interface**
- Connection status checking
- Connection type detection (WiFi, Mobile, Ethernet, etc.)
- Real-time connectivity monitoring via Stream
**NetworkStatus Class**
- Connection state (connected/disconnected)
- Connection type
- Timestamp
- Convenience methods (isWiFi, isMobile, isMetered)
**Riverpod Providers**
- `networkInfoProvider` - NetworkInfo implementation
- `isConnectedProvider` - Current connection status
- `connectionTypeProvider` - Current connection type
- `networkStatusStreamProvider` - Stream of status changes
- `NetworkStatusNotifier` - Reactive network status state
### 4. Error Handling
**Exceptions (`exceptions.dart`)**
- NetworkException - Base network error
- NoInternetException - No connectivity
- TimeoutException - Connection timeout
- ServerException - 5xx errors
- ServiceUnavailableException - 503 errors
- AuthException - Authentication errors (401, 403)
- ValidationException - Request validation errors
- NotFoundException - 404 errors
- ConflictException - 409 errors
- RateLimitException - 429 errors
- PaymentException - Payment-related errors
- CacheException - Cache errors
- StorageException - Local storage errors
- ParseException - JSON parsing errors
**Failures (`failures.dart`)**
- Immutable Freezed classes for domain-level errors
- User-friendly Vietnamese error messages
- Properties:
- `message` - Display message
- `isCritical` - Requires immediate attention
- `canRetry` - Can be retried
- `statusCode` - HTTP status if available
### 5. API Constants (`api_constants.dart`)
**Configuration**
- Base URLs (dev, staging, production)
- API version prefix (/v1)
- Timeout durations (30s)
- Retry configuration (3 attempts, exponential backoff)
- Cache durations (24h products, 1h profile, 48h categories)
- Request headers (JSON, Vietnamese language)
**Endpoints**
- Authentication: /auth/request-otp, /auth/verify-otp, /auth/register, etc.
- Loyalty: /loyalty/points, /loyalty/rewards, /loyalty/referral, etc.
- Products: /products, /products/search, /categories, etc.
- Orders: /orders, /payments, etc.
- Projects & Quotes: /projects, /quotes, etc.
- Chat: /chat/messages, /ws/chat (WebSocket)
- Account: /profile, /addresses, etc.
- Promotions & Notifications
## Usage Examples
### Basic GET Request
```dart
// Using DioClient with Riverpod
final dioClient = ref.watch(dioClientProvider);
try {
final response = await dioClient.get(
ApiConstants.getProducts,
queryParameters: {'page': '1', 'limit': '20'},
);
final products = response.data;
} on NoInternetException catch (e) {
// Handle no internet
} on ServerException catch (e) {
// Handle server error
}
```
### POST Request with Authentication
```dart
final dioClient = ref.watch(dioClientProvider);
try {
final response = await dioClient.post(
ApiConstants.createOrder,
data: {
'items': [...],
'deliveryAddress': {...},
'paymentMethod': 'COD',
},
);
final order = Order.fromJson(response.data);
} on ValidationException catch (e) {
// Handle validation errors
print(e.errors); // Map<String, List<String>>
}
```
### File Upload
```dart
final dioClient = ref.watch(dioClientProvider);
final formData = FormData.fromMap({
'name': 'John Doe',
'avatar': await MultipartFile.fromFile(
filePath,
filename: 'avatar.jpg',
),
});
try {
final response = await dioClient.uploadFile(
ApiConstants.uploadAvatar,
formData: formData,
onSendProgress: (sent, total) {
print('Upload progress: ${(sent / total * 100).toStringAsFixed(0)}%');
},
);
} catch (e) {
// Handle error
}
```
### Network Status Monitoring
```dart
// Check current connection status
final isConnected = await ref.watch(isConnectedProvider.future);
if (!isConnected) {
// Show offline message
}
// Listen to connection changes
ref.listen(
networkStatusStreamProvider,
(previous, next) {
next.whenData((status) {
if (status.isConnected) {
// Back online - sync data
} else {
// Offline - show message
}
});
},
);
```
### Cache Management
```dart
final dioClient = ref.watch(dioClientProvider);
// Clear all cache
await dioClient.clearCache();
// Clear specific endpoint cache
await dioClient.clearCacheByPath(ApiConstants.getProducts);
// Force refresh from network
final response = await dioClient.get(
ApiConstants.getProducts,
options: ApiRequestOptions.forceNetwork.toDioOptions(),
);
// Use cache-first strategy
final response = await dioClient.get(
ApiConstants.getCategories,
options: ApiRequestOptions.cached.toDioOptions(),
);
```
### Custom Error Handling
```dart
try {
final response = await dioClient.post(...);
} on ValidationException catch (e) {
// Show field-specific errors
e.errors?.forEach((field, messages) {
print('$field: ${messages.join(", ")}');
});
} on RateLimitException catch (e) {
// Show rate limit message
if (e.retryAfter != null) {
print('Try again in ${e.retryAfter} seconds');
}
} on TokenExpiredException catch (e) {
// Token refresh failed - redirect to login
ref.read(authProvider.notifier).logout();
} catch (e) {
// Generic error
print('Error: $e');
}
```
## Dependencies
```yaml
dependencies:
dio: ^5.4.3+1 # HTTP client
connectivity_plus: ^6.0.3 # Network monitoring
pretty_dio_logger: ^1.3.1 # Request/response logging
dio_cache_interceptor: ^3.5.0 # Response caching
dio_cache_interceptor_hive_store: ^3.2.2 # Hive storage for cache
flutter_riverpod: ^3.0.0 # State management
riverpod_annotation: ^3.0.0 # Code generation
shared_preferences: ^2.2.3 # Token storage
path_provider: ^2.1.3 # Cache directory
freezed_annotation: ^3.0.0 # Immutable models
```
## Configuration
### Environment-Specific Base URLs
Update `ApiConstants.baseUrl` based on build flavor:
```dart
// For dev environment
static const String baseUrl = devBaseUrl;
// For production
static const String baseUrl = prodBaseUrl;
```
### Timeout Configuration
Adjust timeouts in `ApiConstants`:
```dart
static const Duration connectionTimeout = Duration(milliseconds: 30000);
static const Duration receiveTimeout = Duration(milliseconds: 30000);
static const Duration sendTimeout = Duration(milliseconds: 30000);
```
### Retry Configuration
Customize retry behavior in `ApiConstants`:
```dart
static const int maxRetryAttempts = 3;
static const Duration initialRetryDelay = Duration(milliseconds: 1000);
static const Duration maxRetryDelay = Duration(milliseconds: 5000);
static const double retryDelayMultiplier = 2.0;
```
### Cache Configuration
Adjust cache settings in `cacheOptionsProvider`:
```dart
CacheOptions(
store: store,
maxStale: const Duration(days: 7),
hitCacheOnErrorExcept: [401, 403],
priority: CachePriority.high,
allowPostMethod: false,
);
```
## Testing
### Connection Testing
```dart
// Test network connectivity
final networkInfo = ref.watch(networkInfoProvider);
final isConnected = await networkInfo.isConnected;
final connectionType = await networkInfo.connectionType;
print('Connected: $isConnected');
print('Type: ${connectionType.displayNameVi}');
```
### API Endpoint Testing
```dart
// Test authentication endpoint
try {
final response = await dioClient.post(
ApiConstants.requestOtp,
data: {'phone': '+84912345678'},
);
print('OTP sent successfully');
} catch (e) {
print('Failed: $e');
}
```
## Best Practices
1. **Always use DioClient**: Don't create raw Dio instances
2. **Handle specific exceptions**: Catch specific error types for better UX
3. **Check connectivity**: Verify network status before critical requests
4. **Use cache strategically**: Cache static data (categories, products)
5. **Monitor network changes**: Listen to connectivity stream for sync
6. **Clear cache appropriately**: Clear on logout, version updates
7. **Log in debug only**: Disable logging in production
8. **Sanitize sensitive data**: Never log passwords, tokens, OTP codes
9. **Use retry wisely**: Don't retry POST/PUT/DELETE by default
10. **Validate responses**: Check response.data structure before parsing
## Future Enhancements
- [ ] Offline request queue implementation
- [ ] Request deduplication
- [ ] GraphQL support
- [ ] WebSocket integration for real-time chat
- [ ] Certificate pinning for security
- [ ] Request compression (gzip)
- [ ] Multi-part upload progress
- [ ] Background sync when network restored
- [ ] Advanced caching strategies (stale-while-revalidate)
- [ ] Request cancellation tokens
## Troubleshooting
### Issue: Token Refresh Loop
**Solution**: Check refresh token expiry and clear auth data if expired
### Issue: Cache Not Working
**Solution**: Verify CacheStore initialization and directory permissions
### Issue: Network Detection Fails
**Solution**: Add required permissions to AndroidManifest.xml and Info.plist
### Issue: Timeout on Large Files
**Solution**: Increase timeout or use download with progress callback
### Issue: Interceptor Order Matters
**Current Order**:
1. Logging (first - logs everything)
2. Auth (adds tokens)
3. Cache (caches responses)
4. Retry (retries failures)
5. Error Transformer (last - transforms errors)
## Support
For issues or questions about the API integration:
- Check logs for detailed error information
- Verify network connectivity using NetworkInfo
- Review interceptor configuration
- Check API endpoint constants
---
**Generated for Worker App**
Version: 1.0.0
Last Updated: 2025-10-17

View File

@@ -0,0 +1,572 @@
/// API interceptors for request/response handling
///
/// Provides interceptors for:
/// - Authentication token injection
/// - Request/response logging
/// - Error transformation
/// - Token refresh handling
library;
import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/errors/exceptions.dart';
part 'api_interceptor.g.dart';
// ============================================================================
// Storage Keys
// ============================================================================
/// Keys for storing auth tokens in SharedPreferences
class AuthStorageKeys {
static const String accessToken = 'auth_access_token';
static const String refreshToken = 'auth_refresh_token';
static const String tokenExpiry = 'auth_token_expiry';
}
// ============================================================================
// Auth Interceptor
// ============================================================================
/// Interceptor for adding authentication tokens to requests
class AuthInterceptor extends Interceptor {
AuthInterceptor(this._prefs, this._dio);
final SharedPreferences _prefs;
final Dio _dio;
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
// Check if this endpoint requires authentication
if (_requiresAuth(options.path)) {
final token = await _getAccessToken();
if (token != null) {
// Add bearer token to headers
options.headers['Authorization'] = 'Bearer $token';
}
}
// Add language header
options.headers['Accept-Language'] = ApiConstants.acceptLanguageVi;
// Add content-type and accept headers if not already set
options.headers['Content-Type'] ??= ApiConstants.contentTypeJson;
options.headers['Accept'] ??= ApiConstants.acceptJson;
handler.next(options);
}
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
// Check if error is 401 Unauthorized
if (err.response?.statusCode == 401) {
// Try to refresh token
final refreshed = await _refreshAccessToken();
if (refreshed) {
// Retry the original request with new token
try {
final response = await _retry(err.requestOptions);
handler.resolve(response);
return;
} catch (e) {
// If retry fails, continue with error
}
}
}
handler.next(err);
}
/// Check if endpoint requires authentication
bool _requiresAuth(String path) {
// Public endpoints that don't require auth
final publicEndpoints = [
ApiConstants.requestOtp,
ApiConstants.verifyOtp,
ApiConstants.register,
];
return !publicEndpoints.any((endpoint) => path.contains(endpoint));
}
/// Get access token from storage
Future<String?> _getAccessToken() async {
return _prefs.getString(AuthStorageKeys.accessToken);
}
/// Get refresh token from storage
Future<String?> _getRefreshToken() async {
return _prefs.getString(AuthStorageKeys.refreshToken);
}
/// Check if token is expired
Future<bool> _isTokenExpired() async {
final expiryString = _prefs.getString(AuthStorageKeys.tokenExpiry);
if (expiryString == null) return true;
final expiry = DateTime.tryParse(expiryString);
if (expiry == null) return true;
return DateTime.now().isAfter(expiry);
}
/// Refresh access token using refresh token
Future<bool> _refreshAccessToken() async {
try {
final refreshToken = await _getRefreshToken();
if (refreshToken == null) {
return false;
}
// Call refresh token endpoint
final response = await _dio.post<Map<String, dynamic>>(
'${ApiConstants.apiBaseUrl}${ApiConstants.refreshToken}',
options: Options(
headers: {
'Authorization': 'Bearer $refreshToken',
},
),
);
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
// Save new tokens
await _prefs.setString(
AuthStorageKeys.accessToken,
data['accessToken'] as String,
);
if (data.containsKey('refreshToken')) {
await _prefs.setString(
AuthStorageKeys.refreshToken,
data['refreshToken'] as String,
);
}
if (data.containsKey('expiresAt')) {
await _prefs.setString(
AuthStorageKeys.tokenExpiry,
data['expiresAt'] as String,
);
}
return true;
}
return false;
} catch (e) {
developer.log(
'Failed to refresh token',
name: 'AuthInterceptor',
error: e,
);
return false;
}
}
/// Retry failed request with new token
Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final token = await _getAccessToken();
final options = Options(
method: requestOptions.method,
headers: {
...requestOptions.headers,
'Authorization': 'Bearer $token',
},
);
return _dio.request(
requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options,
);
}
}
// ============================================================================
// Logging Interceptor
// ============================================================================
/// Interceptor for logging requests and responses in debug mode
class LoggingInterceptor extends Interceptor {
LoggingInterceptor({
this.enableRequestLogging = true,
this.enableResponseLogging = true,
this.enableErrorLogging = true,
});
final bool enableRequestLogging;
final bool enableResponseLogging;
final bool enableErrorLogging;
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
if (enableRequestLogging) {
developer.log(
'╔══════════════════════════════════════════════════════════════',
name: 'HTTP Request',
);
developer.log(
'${options.method} ${options.uri}',
name: 'HTTP Request',
);
developer.log(
'║ Headers: ${_sanitizeHeaders(options.headers)}',
name: 'HTTP Request',
);
if (options.data != null) {
developer.log(
'║ Body: ${_sanitizeBody(options.data)}',
name: 'HTTP Request',
);
}
if (options.queryParameters.isNotEmpty) {
developer.log(
'║ Query Parameters: ${options.queryParameters}',
name: 'HTTP Request',
);
}
developer.log(
'╚══════════════════════════════════════════════════════════════',
name: 'HTTP Request',
);
}
handler.next(options);
}
@override
void onResponse(
Response<dynamic> response,
ResponseInterceptorHandler handler,
) {
if (enableResponseLogging) {
developer.log(
'╔══════════════════════════════════════════════════════════════',
name: 'HTTP Response',
);
developer.log(
'${response.requestOptions.method} ${response.requestOptions.uri}',
name: 'HTTP Response',
);
developer.log(
'║ Status Code: ${response.statusCode}',
name: 'HTTP Response',
);
developer.log(
'║ Data: ${_truncateData(response.data, 500)}',
name: 'HTTP Response',
);
developer.log(
'╚══════════════════════════════════════════════════════════════',
name: 'HTTP Response',
);
}
handler.next(response);
}
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) {
if (enableErrorLogging) {
developer.log(
'╔══════════════════════════════════════════════════════════════',
name: 'HTTP Error',
);
developer.log(
'${err.requestOptions.method} ${err.requestOptions.uri}',
name: 'HTTP Error',
);
developer.log(
'║ Error Type: ${err.type}',
name: 'HTTP Error',
);
developer.log(
'║ Status Code: ${err.response?.statusCode}',
name: 'HTTP Error',
);
developer.log(
'║ Message: ${err.message}',
name: 'HTTP Error',
);
if (err.response?.data != null) {
developer.log(
'║ Error Data: ${_truncateData(err.response?.data, 500)}',
name: 'HTTP Error',
);
}
developer.log(
'╚══════════════════════════════════════════════════════════════',
name: 'HTTP Error',
);
}
handler.next(err);
}
/// Sanitize headers by hiding sensitive information
Map<String, dynamic> _sanitizeHeaders(Map<String, dynamic> headers) {
final sanitized = Map<String, dynamic>.from(headers);
// Hide authorization token
if (sanitized.containsKey('Authorization')) {
sanitized['Authorization'] = '[HIDDEN]';
}
return sanitized;
}
/// Sanitize request body by hiding sensitive fields
dynamic _sanitizeBody(dynamic body) {
if (body is Map) {
final sanitized = Map<dynamic, dynamic>.from(body);
// List of sensitive field names
final sensitiveFields = [
'password',
'otp',
'token',
'accessToken',
'refreshToken',
'secret',
'apiKey',
];
for (final field in sensitiveFields) {
if (sanitized.containsKey(field)) {
sanitized[field] = '[HIDDEN]';
}
}
return sanitized;
}
return body;
}
/// Truncate data for logging to avoid huge logs
String _truncateData(dynamic data, int maxLength) {
final dataStr = data.toString();
if (dataStr.length <= maxLength) {
return dataStr;
}
return '${dataStr.substring(0, maxLength)}... [TRUNCATED]';
}
}
// ============================================================================
// Error Transformer Interceptor
// ============================================================================
/// Interceptor for transforming Dio errors into custom exceptions
class ErrorTransformerInterceptor extends Interceptor {
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) {
Exception exception;
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
exception = const TimeoutException();
break;
case DioExceptionType.connectionError:
exception = const NoInternetException();
break;
case DioExceptionType.badResponse:
exception = _handleBadResponse(err.response);
break;
case DioExceptionType.cancel:
exception = NetworkException('Yêu cầu đã bị hủy');
break;
case DioExceptionType.unknown:
exception = NetworkException(
'Lỗi không xác định: ${err.message}',
);
break;
default:
exception = NetworkException(err.message ?? 'Unknown error');
}
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
type: err.type,
error: exception,
message: exception.toString(),
),
);
}
/// Handle bad response errors based on status code
Exception _handleBadResponse(Response<dynamic>? response) {
if (response == null) {
return const ServerException();
}
final statusCode = response.statusCode ?? 0;
final data = response.data;
// Extract error message from response
String? message;
if (data is Map<String, dynamic>) {
message = data['message'] as String? ??
data['error'] as String? ??
data['msg'] as String?;
}
switch (statusCode) {
case 400:
if (data is Map<String, dynamic> && data.containsKey('errors')) {
final errors = data['errors'] as Map<String, dynamic>?;
if (errors != null) {
final validationErrors = errors.map(
(key, value) => MapEntry(
key,
value is List
? value.cast<String>()
: [value.toString()],
),
);
return ValidationException(
message ?? 'Dữ liệu không hợp lệ',
errors: validationErrors,
);
}
}
return BadRequestException(message ?? 'Yêu cầu không hợp lệ');
case 401:
if (message?.toLowerCase().contains('token') ?? false) {
return const TokenExpiredException();
}
if (message?.toLowerCase().contains('otp') ?? false) {
return const InvalidOTPException();
}
return UnauthorizedException(message ?? 'Phiên đăng nhập hết hạn');
case 403:
return const ForbiddenException();
case 404:
return NotFoundException(message ?? 'Không tìm thấy tài nguyên');
case 409:
return ConflictException(message ?? 'Tài nguyên đã tồn tại');
case 422:
if (data is Map<String, dynamic> && data.containsKey('errors')) {
final errors = data['errors'] as Map<String, dynamic>?;
if (errors != null) {
final validationErrors = errors.map(
(key, value) => MapEntry(
key,
value is List
? value.cast<String>()
: [value.toString()],
),
);
return ValidationException(
message ?? 'Dữ liệu không hợp lệ',
errors: validationErrors,
);
}
}
return ValidationException(message ?? 'Dữ liệu không hợp lệ');
case 429:
final retryAfter = response.headers.value('retry-after');
final retrySeconds = retryAfter != null ? int.tryParse(retryAfter) : null;
return RateLimitException(message ?? 'Quá nhiều yêu cầu', retrySeconds);
case 500:
case 502:
case 503:
case 504:
if (statusCode == 503) {
return const ServiceUnavailableException();
}
return ServerException(message ?? 'Lỗi máy chủ', statusCode);
default:
return NetworkException(
message ?? 'Lỗi mạng không xác định',
statusCode: statusCode,
data: data,
);
}
}
}
// ============================================================================
// Riverpod Providers
// ============================================================================
/// Provider for SharedPreferences instance
@riverpod
Future<SharedPreferences> sharedPreferences(Ref ref) async {
return await SharedPreferences.getInstance();
}
/// Provider for AuthInterceptor
@riverpod
Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
final prefs = await ref.watch(sharedPreferencesProvider.future);
return AuthInterceptor(prefs, dio);
}
/// Provider for LoggingInterceptor
@riverpod
LoggingInterceptor loggingInterceptor(Ref ref) {
// Only enable logging in debug mode
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter
return LoggingInterceptor(
enableRequestLogging: isDebug,
enableResponseLogging: isDebug,
enableErrorLogging: isDebug,
);
}
/// Provider for ErrorTransformerInterceptor
@riverpod
ErrorTransformerInterceptor errorTransformerInterceptor(Ref ref) {
return ErrorTransformerInterceptor();
}

View File

@@ -0,0 +1,246 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'api_interceptor.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for SharedPreferences instance
@ProviderFor(sharedPreferences)
const sharedPreferencesProvider = SharedPreferencesProvider._();
/// Provider for SharedPreferences instance
final class SharedPreferencesProvider
extends
$FunctionalProvider<
AsyncValue<SharedPreferences>,
SharedPreferences,
FutureOr<SharedPreferences>
>
with
$FutureModifier<SharedPreferences>,
$FutureProvider<SharedPreferences> {
/// Provider for SharedPreferences instance
const SharedPreferencesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sharedPreferencesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sharedPreferencesHash();
@$internal
@override
$FutureProviderElement<SharedPreferences> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SharedPreferences> create(Ref ref) {
return sharedPreferences(ref);
}
}
String _$sharedPreferencesHash() => r'dc403fbb1d968c7d5ab4ae1721a29ffe173701c7';
/// Provider for AuthInterceptor
@ProviderFor(authInterceptor)
const authInterceptorProvider = AuthInterceptorFamily._();
/// Provider for AuthInterceptor
final class AuthInterceptorProvider
extends
$FunctionalProvider<
AsyncValue<AuthInterceptor>,
AuthInterceptor,
FutureOr<AuthInterceptor>
>
with $FutureModifier<AuthInterceptor>, $FutureProvider<AuthInterceptor> {
/// Provider for AuthInterceptor
const AuthInterceptorProvider._({
required AuthInterceptorFamily super.from,
required Dio super.argument,
}) : super(
retry: null,
name: r'authInterceptorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authInterceptorHash();
@override
String toString() {
return r'authInterceptorProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<AuthInterceptor> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<AuthInterceptor> create(Ref ref) {
final argument = this.argument as Dio;
return authInterceptor(ref, argument);
}
@override
bool operator ==(Object other) {
return other is AuthInterceptorProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$authInterceptorHash() => r'b54ba9af62c3cd7b922ef4030a8e2debb0220e10';
/// Provider for AuthInterceptor
final class AuthInterceptorFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<AuthInterceptor>, Dio> {
const AuthInterceptorFamily._()
: super(
retry: null,
name: r'authInterceptorProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Provider for AuthInterceptor
AuthInterceptorProvider call(Dio dio) =>
AuthInterceptorProvider._(argument: dio, from: this);
@override
String toString() => r'authInterceptorProvider';
}
/// Provider for LoggingInterceptor
@ProviderFor(loggingInterceptor)
const loggingInterceptorProvider = LoggingInterceptorProvider._();
/// Provider for LoggingInterceptor
final class LoggingInterceptorProvider
extends
$FunctionalProvider<
LoggingInterceptor,
LoggingInterceptor,
LoggingInterceptor
>
with $Provider<LoggingInterceptor> {
/// Provider for LoggingInterceptor
const LoggingInterceptorProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'loggingInterceptorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$loggingInterceptorHash();
@$internal
@override
$ProviderElement<LoggingInterceptor> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
LoggingInterceptor create(Ref ref) {
return loggingInterceptor(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(LoggingInterceptor value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<LoggingInterceptor>(value),
);
}
}
String _$loggingInterceptorHash() =>
r'f3dedaeb3152d5188544232f6f270bb6908c2827';
/// Provider for ErrorTransformerInterceptor
@ProviderFor(errorTransformerInterceptor)
const errorTransformerInterceptorProvider =
ErrorTransformerInterceptorProvider._();
/// Provider for ErrorTransformerInterceptor
final class ErrorTransformerInterceptorProvider
extends
$FunctionalProvider<
ErrorTransformerInterceptor,
ErrorTransformerInterceptor,
ErrorTransformerInterceptor
>
with $Provider<ErrorTransformerInterceptor> {
/// Provider for ErrorTransformerInterceptor
const ErrorTransformerInterceptorProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'errorTransformerInterceptorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$errorTransformerInterceptorHash();
@$internal
@override
$ProviderElement<ErrorTransformerInterceptor> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ErrorTransformerInterceptor create(Ref ref) {
return errorTransformerInterceptor(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ErrorTransformerInterceptor value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ErrorTransformerInterceptor>(value),
);
}
}
String _$errorTransformerInterceptorHash() =>
r'15a14206b96d046054277ee0b8220838e0e9e267';

View File

@@ -0,0 +1,496 @@
/// Dio HTTP client configuration for the Worker app
///
/// Provides a configured Dio instance with interceptors for:
/// - Authentication
/// - Logging
/// - Error handling
/// - Caching
/// - Retry logic
library;
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/api_interceptor.dart';
import 'package:worker/core/network/network_info.dart';
part 'dio_client.g.dart';
// ============================================================================
// Dio Client Configuration
// ============================================================================
/// HTTP client wrapper around Dio with interceptors and configuration
class DioClient {
DioClient(this._dio, this._cacheStore);
final Dio _dio;
final CacheStore? _cacheStore;
/// Get the underlying Dio instance
Dio get dio => _dio;
/// Get the cache store
CacheStore? get cacheStore => _cacheStore;
// ============================================================================
// HTTP Methods
// ============================================================================
/// Perform GET request
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
try {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
} catch (e) {
rethrow;
}
}
/// Perform POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
try {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
} catch (e) {
rethrow;
}
}
/// Perform PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
try {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
} catch (e) {
rethrow;
}
}
/// Perform PATCH request
Future<Response<T>> patch<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
try {
return await _dio.patch<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
} catch (e) {
rethrow;
}
}
/// Perform DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} catch (e) {
rethrow;
}
}
/// Upload file with multipart/form-data
Future<Response<T>> uploadFile<T>(
String path, {
required FormData formData,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
return await _dio.post<T>(
path,
data: formData,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
} catch (e) {
rethrow;
}
}
/// Download file
Future<Response<dynamic>> downloadFile(
String urlPath,
String savePath, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
bool deleteOnError = true,
String lengthHeader = Headers.contentLengthHeader,
Options? options,
}) async {
try {
return await _dio.download(
urlPath,
savePath,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
deleteOnError: deleteOnError,
lengthHeader: lengthHeader,
options: options,
);
} catch (e) {
rethrow;
}
}
// ============================================================================
// Cache Management
// ============================================================================
/// Clear all cached responses
Future<void> clearCache() async {
if (_cacheStore != null) {
await _cacheStore!.clean();
}
}
/// Clear specific cached response by key
Future<void> clearCacheByKey(String key) async {
if (_cacheStore != null) {
await _cacheStore!.delete(key);
}
}
/// Clear cache for specific path
Future<void> clearCacheByPath(String path) async {
if (_cacheStore != null) {
final key = CacheOptions.defaultCacheKeyBuilder(
RequestOptions(path: path),
);
await _cacheStore!.delete(key);
}
}
}
// ============================================================================
// Retry Interceptor
// ============================================================================
/// Interceptor for retrying failed requests with exponential backoff
class RetryInterceptor extends Interceptor {
RetryInterceptor(
this._networkInfo, {
this.maxRetries = ApiConstants.maxRetryAttempts,
this.initialDelay = ApiConstants.initialRetryDelay,
this.maxDelay = ApiConstants.maxRetryDelay,
this.delayMultiplier = ApiConstants.retryDelayMultiplier,
});
final NetworkInfo _networkInfo;
final int maxRetries;
final Duration initialDelay;
final Duration maxDelay;
final double delayMultiplier;
@override
void onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
// Get retry count from request extra
final retries = err.requestOptions.extra['retries'] as int? ?? 0;
// Check if we should retry
if (retries >= maxRetries || !_shouldRetry(err)) {
handler.next(err);
return;
}
// Check network connectivity before retrying
final isConnected = await _networkInfo.isConnected;
if (!isConnected) {
handler.next(err);
return;
}
// Calculate delay with exponential backoff
final delayMs = (initialDelay.inMilliseconds *
(delayMultiplier * (retries + 1))).toInt();
final delay = Duration(
milliseconds: delayMs.clamp(
initialDelay.inMilliseconds,
maxDelay.inMilliseconds,
),
);
// Wait before retry
await Future<void>.delayed(delay);
// Increment retry count
err.requestOptions.extra['retries'] = retries + 1;
// Retry the request
try {
final dio = Dio();
final response = await dio.fetch<dynamic>(err.requestOptions);
handler.resolve(response);
} on DioException catch (e) {
handler.next(e);
}
}
/// Determine if error should trigger a retry
bool _shouldRetry(DioException error) {
// Retry on connection errors
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.connectionError) {
return true;
}
// Retry on 5xx server errors (except 501)
final statusCode = error.response?.statusCode;
if (statusCode != null && statusCode >= 500 && statusCode != 501) {
return true;
}
// Retry on 408 Request Timeout
if (statusCode == 408) {
return true;
}
// Retry on 429 Too Many Requests (with delay from header)
if (statusCode == 429) {
return true;
}
return false;
}
}
// ============================================================================
// Riverpod Providers
// ============================================================================
/// Provider for cache store
@riverpod
Future<CacheStore> cacheStore(Ref ref) async {
final directory = await getTemporaryDirectory();
return HiveCacheStore(
directory.path,
hiveBoxName: 'dio_cache',
);
}
/// Provider for cache options
@riverpod
Future<CacheOptions> cacheOptions(Ref ref) async {
final store = await ref.watch(cacheStoreProvider.future);
return CacheOptions(
store: store,
maxStale: const Duration(days: 7), // Keep cache for 7 days
hitCacheOnErrorExcept: [401, 403], // Use cache on error except auth errors
priority: CachePriority.high,
cipher: null, // No encryption for now
keyBuilder: CacheOptions.defaultCacheKeyBuilder,
allowPostMethod: false, // Don't cache POST requests by default
);
}
/// Provider for Dio instance with all interceptors
@riverpod
Future<Dio> dio(Ref ref) async {
final dio = Dio();
// Base configuration
dio
..options = BaseOptions(
baseUrl: ApiConstants.apiBaseUrl,
connectTimeout: ApiConstants.connectionTimeout,
receiveTimeout: ApiConstants.receiveTimeout,
sendTimeout: ApiConstants.sendTimeout,
headers: {
'Content-Type': ApiConstants.contentTypeJson,
'Accept': ApiConstants.acceptJson,
'Accept-Language': ApiConstants.acceptLanguageVi,
},
responseType: ResponseType.json,
validateStatus: (status) {
// Accept all status codes and handle errors in interceptor
return status != null && status < 500;
},
)
// Add interceptors in order
// 1. Logging interceptor (first to log everything)
..interceptors.add(ref.watch(loggingInterceptorProvider))
// 2. Auth interceptor (add tokens to requests)
..interceptors.add(await ref.watch(authInterceptorProvider(dio).future))
// 3. Cache interceptor
..interceptors.add(DioCacheInterceptor(options: await ref.watch(cacheOptionsProvider.future)))
// 4. Retry interceptor
..interceptors.add(RetryInterceptor(ref.watch(networkInfoProvider)))
// 5. Error transformer (last to transform all errors)
..interceptors.add(ref.watch(errorTransformerInterceptorProvider));
return dio;
}
/// Provider for DioClient
@riverpod
Future<DioClient> dioClient(Ref ref) async {
final dio = await ref.watch(dioProvider.future);
final cacheStore = await ref.watch(cacheStoreProvider.future);
return DioClient(dio, cacheStore);
}
// ============================================================================
// Helper Classes
// ============================================================================
/// Options for API requests with custom cache policy
class ApiRequestOptions {
const ApiRequestOptions({
this.cachePolicy,
this.cacheDuration,
this.forceRefresh = false,
});
final CachePolicy? cachePolicy;
final Duration? cacheDuration;
final bool forceRefresh;
/// Options with cache enabled
static const cached = ApiRequestOptions(
cachePolicy: CachePolicy.forceCache,
);
/// Options with network-first strategy
static const networkFirst = ApiRequestOptions(
cachePolicy: CachePolicy.refreshForceCache,
);
/// Options to force refresh from network
static const forceNetwork = ApiRequestOptions(
cachePolicy: CachePolicy.refresh,
forceRefresh: true,
);
/// Convert to Dio Options
Options toDioOptions() {
return Options(
extra: <String, dynamic>{
if (cachePolicy != null)
CacheResponse.cacheKey: cachePolicy!.index,
if (cacheDuration != null)
'maxStale': cacheDuration,
if (forceRefresh)
'policy': CachePolicy.refresh.index,
},
);
}
}
/// Offline request queue item
class QueuedRequest {
QueuedRequest({
required this.method,
required this.path,
this.data,
this.queryParameters,
required this.timestamp,
});
factory QueuedRequest.fromJson(Map<String, dynamic> json) {
return QueuedRequest(
method: json['method'] as String,
path: json['path'] as String,
data: json['data'],
queryParameters: json['queryParameters'] as Map<String, dynamic>?,
timestamp: DateTime.parse(json['timestamp'] as String),
);
}
final String method;
final String path;
final dynamic data;
final Map<String, dynamic>? queryParameters;
final DateTime timestamp;
Map<String, dynamic> toJson() => <String, dynamic>{
'method': method,
'path': path,
'data': data,
'queryParameters': queryParameters,
'timestamp': timestamp.toIso8601String(),
};
}

View File

@@ -0,0 +1,177 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dio_client.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for cache store
@ProviderFor(cacheStore)
const cacheStoreProvider = CacheStoreProvider._();
/// Provider for cache store
final class CacheStoreProvider
extends
$FunctionalProvider<
AsyncValue<CacheStore>,
CacheStore,
FutureOr<CacheStore>
>
with $FutureModifier<CacheStore>, $FutureProvider<CacheStore> {
/// Provider for cache store
const CacheStoreProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'cacheStoreProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$cacheStoreHash();
@$internal
@override
$FutureProviderElement<CacheStore> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<CacheStore> create(Ref ref) {
return cacheStore(ref);
}
}
String _$cacheStoreHash() => r'8cbc2688ee267e03fc5aa6bf48c3ada249cb6345';
/// Provider for cache options
@ProviderFor(cacheOptions)
const cacheOptionsProvider = CacheOptionsProvider._();
/// Provider for cache options
final class CacheOptionsProvider
extends
$FunctionalProvider<
AsyncValue<CacheOptions>,
CacheOptions,
FutureOr<CacheOptions>
>
with $FutureModifier<CacheOptions>, $FutureProvider<CacheOptions> {
/// Provider for cache options
const CacheOptionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'cacheOptionsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$cacheOptionsHash();
@$internal
@override
$FutureProviderElement<CacheOptions> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<CacheOptions> create(Ref ref) {
return cacheOptions(ref);
}
}
String _$cacheOptionsHash() => r'6b6b951855d8c0094e36918efa79c6ba586e156d';
/// Provider for Dio instance with all interceptors
@ProviderFor(dio)
const dioProvider = DioProvider._();
/// Provider for Dio instance with all interceptors
final class DioProvider
extends $FunctionalProvider<AsyncValue<Dio>, Dio, FutureOr<Dio>>
with $FutureModifier<Dio>, $FutureProvider<Dio> {
/// Provider for Dio instance with all interceptors
const DioProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dioProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dioHash();
@$internal
@override
$FutureProviderElement<Dio> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Dio> create(Ref ref) {
return dio(ref);
}
}
String _$dioHash() => r'f15495e99d11744c245e2be892657748aeeb8ae7';
/// Provider for DioClient
@ProviderFor(dioClient)
const dioClientProvider = DioClientProvider._();
/// Provider for DioClient
final class DioClientProvider
extends
$FunctionalProvider<
AsyncValue<DioClient>,
DioClient,
FutureOr<DioClient>
>
with $FutureModifier<DioClient>, $FutureProvider<DioClient> {
/// Provider for DioClient
const DioClientProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dioClientProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dioClientHash();
@$internal
@override
$FutureProviderElement<DioClient> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<DioClient> create(Ref ref) {
return dioClient(ref);
}
}
String _$dioClientHash() => r'4f6754880ccc00aa99b8ae19904e9da88950a4e1';

View File

@@ -0,0 +1,365 @@
/// Network connectivity information and monitoring
///
/// Provides real-time network status checking, connection type detection,
/// and connectivity monitoring for the Worker app.
library;
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'network_info.g.dart';
// ============================================================================
// Network Connection Types
// ============================================================================
/// Types of network connections
enum NetworkConnectionType {
/// WiFi connection
wifi,
/// Mobile data connection
mobile,
/// Ethernet connection (wired)
ethernet,
/// Bluetooth connection
bluetooth,
/// VPN connection
vpn,
/// No connection
none,
/// Unknown connection type
unknown,
}
// ============================================================================
// Network Status
// ============================================================================
/// Network connectivity status
class NetworkStatus {
const NetworkStatus({
required this.isConnected,
required this.connectionType,
required this.timestamp,
});
factory NetworkStatus.connected(NetworkConnectionType type) {
return NetworkStatus(
isConnected: true,
connectionType: type,
timestamp: DateTime.now(),
);
}
factory NetworkStatus.disconnected() {
return NetworkStatus(
isConnected: false,
connectionType: NetworkConnectionType.none,
timestamp: DateTime.now(),
);
}
final bool isConnected;
final NetworkConnectionType connectionType;
final DateTime timestamp;
/// Check if connected via WiFi
bool get isWiFi => connectionType == NetworkConnectionType.wifi;
/// Check if connected via mobile data
bool get isMobile => connectionType == NetworkConnectionType.mobile;
/// Check if connected via ethernet
bool get isEthernet => connectionType == NetworkConnectionType.ethernet;
/// Check if connection is metered (mobile data)
bool get isMetered => isMobile;
@override
String toString() {
return 'NetworkStatus(isConnected: $isConnected, type: $connectionType)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is NetworkStatus &&
other.isConnected == isConnected &&
other.connectionType == connectionType;
}
@override
int get hashCode => Object.hash(isConnected, connectionType);
}
// ============================================================================
// Network Info Interface
// ============================================================================
/// Abstract interface for network information
abstract class NetworkInfo {
/// Check if device is currently connected to internet
Future<bool> get isConnected;
/// Get current network connection type
Future<NetworkConnectionType> get connectionType;
/// Get current network status
Future<NetworkStatus> get networkStatus;
/// Stream of network status changes
Stream<NetworkStatus> get onNetworkStatusChanged;
}
// ============================================================================
// Network Info Implementation
// ============================================================================
/// Implementation of NetworkInfo using connectivity_plus
class NetworkInfoImpl implements NetworkInfo {
NetworkInfoImpl(this._connectivity);
final Connectivity _connectivity;
StreamController<NetworkStatus>? _statusController;
StreamSubscription<List<ConnectivityResult>>? _subscription;
@override
Future<bool> get isConnected async {
final results = await _connectivity.checkConnectivity();
return _hasConnection(results);
}
@override
Future<NetworkConnectionType> get connectionType async {
final results = await _connectivity.checkConnectivity();
return _mapConnectivityResult(results);
}
@override
Future<NetworkStatus> get networkStatus async {
final results = await _connectivity.checkConnectivity();
final hasConnection = _hasConnection(results);
final type = _mapConnectivityResult(results);
if (hasConnection) {
return NetworkStatus.connected(type);
} else {
return NetworkStatus.disconnected();
}
}
@override
Stream<NetworkStatus> get onNetworkStatusChanged {
_statusController ??= StreamController<NetworkStatus>.broadcast(
onListen: _startListening,
onCancel: _stopListening,
);
return _statusController!.stream;
}
void _startListening() {
_subscription = _connectivity.onConnectivityChanged.listen(
(results) {
final hasConnection = _hasConnection(results);
final type = _mapConnectivityResult(results);
final status = hasConnection
? NetworkStatus.connected(type)
: NetworkStatus.disconnected();
_statusController?.add(status);
},
onError: (error) {
_statusController?.add(NetworkStatus.disconnected());
},
);
}
void _stopListening() {
_subscription?.cancel();
_subscription = null;
}
bool _hasConnection(List<ConnectivityResult> results) {
if (results.isEmpty) return false;
return !results.contains(ConnectivityResult.none);
}
NetworkConnectionType _mapConnectivityResult(List<ConnectivityResult> results) {
if (results.isEmpty || results.contains(ConnectivityResult.none)) {
return NetworkConnectionType.none;
}
// Priority order: WiFi > Ethernet > Mobile > Bluetooth > VPN
if (results.contains(ConnectivityResult.wifi)) {
return NetworkConnectionType.wifi;
} else if (results.contains(ConnectivityResult.ethernet)) {
return NetworkConnectionType.ethernet;
} else if (results.contains(ConnectivityResult.mobile)) {
return NetworkConnectionType.mobile;
} else if (results.contains(ConnectivityResult.bluetooth)) {
return NetworkConnectionType.bluetooth;
} else if (results.contains(ConnectivityResult.vpn)) {
return NetworkConnectionType.vpn;
} else {
return NetworkConnectionType.unknown;
}
}
/// Dispose resources
void dispose() {
_subscription?.cancel();
_statusController?.close();
}
}
// ============================================================================
// Riverpod Providers
// ============================================================================
/// Provider for Connectivity instance
@riverpod
Connectivity connectivity(Ref ref) {
return Connectivity();
}
/// Provider for NetworkInfo instance
@riverpod
NetworkInfo networkInfo(Ref ref) {
final connectivity = ref.watch(connectivityProvider);
final networkInfo = NetworkInfoImpl(connectivity);
// Dispose when provider is disposed
ref.onDispose(() {
networkInfo.dispose();
});
return networkInfo;
}
/// Provider for current network connection status (boolean)
@riverpod
Future<bool> isConnected(Ref ref) async {
final networkInfo = ref.watch(networkInfoProvider);
return await networkInfo.isConnected;
}
/// Provider for current network connection type
@riverpod
Future<NetworkConnectionType> connectionType(Ref ref) async {
final networkInfo = ref.watch(networkInfoProvider);
return await networkInfo.connectionType;
}
/// Stream provider for network status changes
@riverpod
Stream<NetworkStatus> networkStatusStream(Ref ref) {
final networkInfo = ref.watch(networkInfoProvider);
return networkInfo.onNetworkStatusChanged;
}
/// Provider for current network status
@riverpod
class NetworkStatusNotifier extends _$NetworkStatusNotifier {
@override
Future<NetworkStatus> build() async {
final networkInfo = ref.watch(networkInfoProvider);
final status = await networkInfo.networkStatus;
// Listen to network changes
ref.listen(
networkStatusStreamProvider,
(_, next) {
next.whenData((newStatus) {
state = AsyncValue.data(newStatus);
});
},
);
return status;
}
/// Manually refresh network status
Future<void> refresh() async {
state = const AsyncValue.loading();
final networkInfo = ref.read(networkInfoProvider);
state = await AsyncValue.guard(() => networkInfo.networkStatus);
}
/// Check if connected
bool get isConnected {
return state.when(
data: (status) => status.isConnected,
loading: () => false,
error: (_, __) => false,
);
}
/// Get connection type
NetworkConnectionType get type {
return state.when(
data: (status) => status.connectionType,
loading: () => NetworkConnectionType.none,
error: (_, __) => NetworkConnectionType.none,
);
}
}
// ============================================================================
// Utility Extensions
// ============================================================================
/// Extension methods for NetworkConnectionType
extension NetworkConnectionTypeX on NetworkConnectionType {
/// Get display name in Vietnamese
String get displayNameVi {
switch (this) {
case NetworkConnectionType.wifi:
return 'WiFi';
case NetworkConnectionType.mobile:
return 'Dữ liệu di động';
case NetworkConnectionType.ethernet:
return 'Ethernet';
case NetworkConnectionType.bluetooth:
return 'Bluetooth';
case NetworkConnectionType.vpn:
return 'VPN';
case NetworkConnectionType.none:
return 'Không có kết nối';
case NetworkConnectionType.unknown:
return 'Không xác định';
}
}
/// Get display name in English
String get displayNameEn {
switch (this) {
case NetworkConnectionType.wifi:
return 'WiFi';
case NetworkConnectionType.mobile:
return 'Mobile Data';
case NetworkConnectionType.ethernet:
return 'Ethernet';
case NetworkConnectionType.bluetooth:
return 'Bluetooth';
case NetworkConnectionType.vpn:
return 'VPN';
case NetworkConnectionType.none:
return 'No Connection';
case NetworkConnectionType.unknown:
return 'Unknown';
}
}
/// Check if this is a valid connection type
bool get isValid {
return this != NetworkConnectionType.none &&
this != NetworkConnectionType.unknown;
}
}

View File

@@ -0,0 +1,282 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'network_info.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for Connectivity instance
@ProviderFor(connectivity)
const connectivityProvider = ConnectivityProvider._();
/// Provider for Connectivity instance
final class ConnectivityProvider
extends $FunctionalProvider<Connectivity, Connectivity, Connectivity>
with $Provider<Connectivity> {
/// Provider for Connectivity instance
const ConnectivityProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'connectivityProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$connectivityHash();
@$internal
@override
$ProviderElement<Connectivity> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Connectivity create(Ref ref) {
return connectivity(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Connectivity value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Connectivity>(value),
);
}
}
String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
/// Provider for NetworkInfo instance
@ProviderFor(networkInfo)
const networkInfoProvider = NetworkInfoProvider._();
/// Provider for NetworkInfo instance
final class NetworkInfoProvider
extends $FunctionalProvider<NetworkInfo, NetworkInfo, NetworkInfo>
with $Provider<NetworkInfo> {
/// Provider for NetworkInfo instance
const NetworkInfoProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'networkInfoProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$networkInfoHash();
@$internal
@override
$ProviderElement<NetworkInfo> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
NetworkInfo create(Ref ref) {
return networkInfo(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NetworkInfo value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NetworkInfo>(value),
);
}
}
String _$networkInfoHash() => r'aee276b1536c8c994273dbed1909a2c24a7c71d2';
/// Provider for current network connection status (boolean)
@ProviderFor(isConnected)
const isConnectedProvider = IsConnectedProvider._();
/// Provider for current network connection status (boolean)
final class IsConnectedProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
/// Provider for current network connection status (boolean)
const IsConnectedProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'isConnectedProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isConnectedHash();
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
return isConnected(ref);
}
}
String _$isConnectedHash() => r'c9620cadbcdee8e738f865e747dd57262236782d';
/// Provider for current network connection type
@ProviderFor(connectionType)
const connectionTypeProvider = ConnectionTypeProvider._();
/// Provider for current network connection type
final class ConnectionTypeProvider
extends
$FunctionalProvider<
AsyncValue<NetworkConnectionType>,
NetworkConnectionType,
FutureOr<NetworkConnectionType>
>
with
$FutureModifier<NetworkConnectionType>,
$FutureProvider<NetworkConnectionType> {
/// Provider for current network connection type
const ConnectionTypeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'connectionTypeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$connectionTypeHash();
@$internal
@override
$FutureProviderElement<NetworkConnectionType> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<NetworkConnectionType> create(Ref ref) {
return connectionType(ref);
}
}
String _$connectionTypeHash() => r'413aead6c4ff6f2c1476e4795934fddb76b797e6';
/// Stream provider for network status changes
@ProviderFor(networkStatusStream)
const networkStatusStreamProvider = NetworkStatusStreamProvider._();
/// Stream provider for network status changes
final class NetworkStatusStreamProvider
extends
$FunctionalProvider<
AsyncValue<NetworkStatus>,
NetworkStatus,
Stream<NetworkStatus>
>
with $FutureModifier<NetworkStatus>, $StreamProvider<NetworkStatus> {
/// Stream provider for network status changes
const NetworkStatusStreamProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'networkStatusStreamProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$networkStatusStreamHash();
@$internal
@override
$StreamProviderElement<NetworkStatus> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<NetworkStatus> create(Ref ref) {
return networkStatusStream(ref);
}
}
String _$networkStatusStreamHash() =>
r'bdff8d93b214ebc290e81ab72fb8d51d8bfb27b1';
/// Provider for current network status
@ProviderFor(NetworkStatusNotifier)
const networkStatusProvider = NetworkStatusNotifierProvider._();
/// Provider for current network status
final class NetworkStatusNotifierProvider
extends $AsyncNotifierProvider<NetworkStatusNotifier, NetworkStatus> {
/// Provider for current network status
const NetworkStatusNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'networkStatusProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$networkStatusNotifierHash();
@$internal
@override
NetworkStatusNotifier create() => NetworkStatusNotifier();
}
String _$networkStatusNotifierHash() =>
r'628e313a66129282cd06dfdd561af3f0a4517b4f';
/// Provider for current network status
abstract class _$NetworkStatusNotifier extends $AsyncNotifier<NetworkStatus> {
FutureOr<NetworkStatus> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<AsyncValue<NetworkStatus>, NetworkStatus>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<NetworkStatus>, NetworkStatus>,
AsyncValue<NetworkStatus>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}