A powerful Flutter package that enhances Dio HTTP client with built-in support for caching, authentication, pagination, error handling, and standardized JSON utilities. Built for modern Flutter applications that need robust API integration.
- Features
- Installation
- Getting Started
- Core Components
- Making Requests
- Response Handling
- Authentication
- Advanced Features
- Best Practices
- Troubleshooting
- Contributing
- License
- π Modern HTTP Client: Built on top of Dio with enhanced features
- π Smart Response Handling: Automatic conversion of responses to strongly-typed models
- πΎ Intelligent Caching: Built-in response caching with configurable TTL
- π Token Management: Robust authentication with token refresh support
- π Auto-Retry: Configurable retry logic for failed requests
- β‘ Rate Limiting: Prevent API throttling with built-in rate limiting
- πΆ Network Awareness: Automatic handling of connectivity changes
- π Request Metrics: Built-in performance tracking
- π Detailed Logging: Complete request/response logging with cURL commands
- π Pagination Support: Built-in utilities for handling paginated responses
- π‘οΈ Type Safety: Strong typing throughout the library
- π― Error Handling: Comprehensive error handling with typed error responses
Add to your pubspec.yaml:
dependencies:
dio_flow: ^1.1.7
import 'package:dio_flow/dio_flow.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Configure the client
DioFlowConfig.initialize(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
);
// 2. Initialize the client
await ApiClient.initialize();
runApp(MyApp());
}
The main class for making HTTP requests:
// GET request
final response = await DioRequestHandler.get(
'users',
parameters: {'role': 'admin'},
requestOptions: RequestOptionsModel(
hasBearerToken: true,
shouldCache: true,
retryOptions: RetryOptions(
maxAttempts: 3,
retryInterval: const Duration(seconds: 1),
),
),
);
// POST request with typed response
final loginResponse = await DioRequestHandler.post<LoginResponse>(
'auth/login',
data: {
'email': 'user@example.com',
'password': '********',
},
requestOptions: RequestOptionsModel(
hasBearerToken: false,
),
);
All responses are wrapped in typed models:
if (response.isSuccess) {
final data = response.data;
// Handle success
} else {
final error = response.error;
switch (error.errorType) {
case ErrorType.network:
// Handle network error
break;
case ErrorType.validation:
// Handle validation error
break;
case ErrorType.unauthorized:
// Handle auth error
break;
// ... handle other error types
}
}
The package includes several built-in interceptors:
- MetricsInterceptor: Tracks request performance
- RateLimitInterceptor: Prevents API throttling
- DioInterceptor: Handles authentication and headers
- RetryInterceptor: Manages request retries
- ConnectivityInterceptor: Handles network state
- CacheInterceptor: Manages response caching
The package provides robust token management with persistent storage:
// Initialize token manager (call this in your main.dart)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await TokenManager.initialize();
runApp(MyApp());
}
// Setting tokens with persistence
await TokenManager.setTokens(
accessToken: 'your_access_token',
refreshToken: 'your_refresh_token',
expiry: DateTime.now().add(Duration(hours: 1)),
);
// Getting access token (automatically handles refresh if needed)
final token = await TokenManager.getAccessToken();
// Clearing tokens
await TokenManager.clearTokens();
Key features:
- Persistent token storage using SharedPreferences
- Automatic token loading on app initialization
- Token expiry tracking and automatic refresh
- Secure token management with proper error handling
- Asynchronous operations for better performance
// Make authenticated request
final response = await DioRequestHandler.get(
'user/profile',
requestOptions: RequestOptionsModel(
hasBearerToken: true, // This will automatically include the token
),
);
// Handle token expiration
if (response.error?.errorType == ErrorType.unauthorized) {
// Token expired, handle refresh or logout
}
// Register endpoints
EndpointProvider.instance.register('login', '/auth/login');
EndpointProvider.instance.register('users', '/api/users');
// Use registered endpoints
final response = await DioRequestHandler.post(
'login',
data: {'email': email, 'password': password},
);
// Register endpoint with parameters
EndpointProvider.instance.register('user_details', '/api/users/{id}');
// Use with path parameters
final response = await DioRequestHandler.get(
'user_details',
pathParameters: {'id': '123'},
);
// Enable caching for a request
final response = await DioRequestHandler.get(
'users',
requestOptions: RequestOptionsModel(
shouldCache: true,
cacheMaxAge: const Duration(minutes: 5),
),
);
// Clear cache
await ApiClient.clearCache();
// Using pagination utilities
final paginatedResponse = await DioRequestHandler.get(
'posts',
parameters: {
'page': 1,
'per_page': 20,
},
);
final pagination = PaginationHelper.fromResponse(paginatedResponse);
final hasMore = pagination.hasNextPage;
final totalPages = pagination.totalPages;
// Safe JSON parsing
final jsonData = JsonUtils.tryParseJson(rawJson);
// Access nested values safely
final nestedValue = JsonUtils.getNestedValue(
jsonData,
'user.profile.name',
'Default Name',
);
// Queue multiple requests
final responses = await Future.wait([
DioRequestHandler.get('users'),
DioRequestHandler.get('posts'),
DioRequestHandler.get('comments'),
]);
// Handle rate limiting automatically
final rateLimitedResponse = await DioRequestHandler.get(
'high-frequency-endpoint',
requestOptions: RequestOptionsModel(
shouldRateLimit: true,
rateLimit: 30, // requests per minute
),
);
class PaginatedResponse<T> {
final List<T> items;
final int total;
final int page;
PaginatedResponse.fromJson(
Map<String, dynamic> json,
T Function(Map<String, dynamic>) converter,
) : items = (json['data'] as List)
.map((item) => converter(item))
.toList(),
total = json['total'] ?? 0,
page = json['page'] ?? 1;
}
// Use with typed response
final response = await DioRequestHandler.get<PaginatedResponse<User>>(
'users',
converter: (json) => PaginatedResponse.fromJson(
json,
(item) => User.fromJson(item),
),
);
-
Initialize Early:
void main() async { await ApiClient.initialize(); // ... rest of your app initialization }
-
Handle Errors Consistently:
try { final response = await DioRequestHandler.get('endpoint'); if (response.isSuccess) { // Handle success } else { // Use the typed error handling handleError(response.error); } } catch (e) { // Handle unexpected errors }
-
Use Type-Safe Responses:
class UserResponse { final String id; final String name; UserResponse.fromJson(Map<String, dynamic> json) : id = json['id'], name = json['name']; } final response = await DioRequestHandler.get<UserResponse>( 'users/me', converter: (json) => UserResponse.fromJson(json), );
// Create a reusable error handler
Future<T> handleApiResponse<T>(ResponseModel response) async {
if (response.isSuccess) {
return response.data as T;
}
switch (response.error?.errorType) {
case ErrorType.network:
throw NetworkException(response.error!.message);
case ErrorType.unauthorized:
await handleUnauthorized();
throw AuthException(response.error!.message);
case ErrorType.validation:
throw ValidationException(response.error!.message);
default:
throw ApiException(response.error?.message ?? 'Unknown error');
}
}
// Use in your code
try {
final users = await handleApiResponse<List<User>>(
await DioRequestHandler.get('users'),
);
// Use users data
} on NetworkException catch (e) {
// Handle network error
} on AuthException catch (e) {
// Handle auth error
} on ValidationException catch (e) {
// Handle validation error
} on ApiException catch (e) {
// Handle other API errors
}
class UserRepository {
Future<User> getCurrentUser() async {
final response = await DioRequestHandler.get<User>(
'users/me',
requestOptions: RequestOptionsModel(
hasBearerToken: true,
shouldCache: true,
cacheMaxAge: const Duration(minutes: 5),
),
converter: (json) => User.fromJson(json),
);
return handleApiResponse<User>(response);
}
Future<void> updateProfile(UserUpdateRequest request) async {
final response = await DioRequestHandler.put(
'users/me',
data: request.toJson(),
requestOptions: RequestOptionsModel(hasBearerToken: true),
);
await handleApiResponse(response);
}
}
Common issues and solutions:
-
Authentication Issues:
- Ensure
hasBearerToken
is set correctly inRequestOptionsModel
- Check if tokens are properly managed in
TokenManager
- Ensure
-
Caching Problems:
- Verify
shouldCache
is enabled in request options - Check cache duration settings
- Try clearing cache with
ApiClient.clearCache()
- Verify
-
Network Errors:
- Check connectivity status
- Verify retry options are configured
- Examine cURL logs for request details
// Enable detailed logging
DioFlowConfig.initialize(
baseUrl: 'https://api.example.com',
debugMode: true, // This will enable detailed logging
);
// Log specific requests
final response = await DioRequestHandler.get(
'users',
requestOptions: RequestOptionsModel(
shouldLogRequest: true, // Log this specific request
),
);
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.