A powerful, production-ready Flutter package that supercharges Dio HTTP client
Built for modern Flutter applications that demand robust, scalable API integration
π Documentation β’ π Quick Start β’ π‘ Examples β’ π Issues
Dio Flow transforms your API integration experience by providing enterprise-grade features out of the box:
- π₯ Zero Configuration: Get started in minutes with sensible defaults
- π‘οΈ Production Ready: Battle-tested with comprehensive error handling
- π Performance First: Built-in caching, retry logic, and request optimization
- π Universal: Works seamlessly across all Flutter platforms
- π§ͺ Developer Friendly: Extensive mocking support for testing
- π Observable: Built-in metrics and detailed logging
- π― Why Dio Flow?
- β¨ Features
- π― Platform Support
- π¦ Installation
- π Getting Started
- π― Core Components
- οΏ½ Authentication
- π Endpoint Configuration
- π Advanced Features
- οΏ½οΈ uBest Practices
- π§ͺ Mock Support
- οΏ½ GraphQL Support
- οΏ½ FBile Operations
- π Troubleshooting
- π€ Contributing
- οΏ½ LicenseOse
π Core | π Security | π οΈ Developer Experience |
---|---|---|
Modern HTTP Client | Token Management | Type-Safe Responses |
Smart Response Handling | Auto Token Refresh | Comprehensive Testing |
Intelligent Caching | Request Authentication | Built-in Mocking |
Auto-Retry Logic | Secure Token Storage | Detailed Logging |
π Platform | π Performance | π§ Advanced |
---|---|---|
Universal Support | Request Metrics | GraphQL Support |
Web Compatible | Rate Limiting | File Operations |
Cross-Platform | Network Awareness | Pagination Utils |
WASM Ready | Connection Pooling | JSON Utilities |
- π Modern HTTP Client: Enhanced Dio with production-grade features
- π Smart Response Handling: Automatic conversion to strongly-typed models
- πΎ Intelligent Caching: Configurable TTL with automatic invalidation
- π Token Management: Robust authentication with automatic refresh
- π Auto-Retry: Configurable retry logic with exponential backoff
- β‘ Rate Limiting: Built-in throttling to prevent API abuse
- πΆ Network Awareness: Automatic connectivity change handling
- π Request Metrics: Built-in performance tracking and analytics
- π Detailed Logging: Complete request/response logging with cURL commands
- π Pagination Support: Utilities for handling paginated API responses
- π‘οΈ Type Safety: Strong typing throughout the entire library
- π― Error Handling: Comprehensive error handling with typed responses
- π§ͺ Mock Support: Built-in mocking system for testing without real HTTP calls
- π GraphQL Support: Native GraphQL queries, mutations, and subscriptions
- π File Operations: Easy file upload/download with progress tracking
- π§ Extensible Architecture: Plugin-based design for custom functionality
Platform | Support | File Operations | Network Checking | WASM Compatible |
---|---|---|---|---|
π± iOS | β Full | β Complete | β Native | β Ready |
π€ Android | β Full | β Complete | β Native | β Ready |
π Web | β Full | π Bytes Only* | β HTTP-based | β Compatible |
πͺ Windows | β Full | β Complete | β Native | β Ready |
π macOS | β Full | β Complete | β Native | β Ready |
π§ Linux | β Full | β Complete | β Native | β Ready |
π‘ Web Platform Note: File operations use
Uint8List
instead ofFile
objects due to browser security restrictions. This ensures maximum compatibility while maintaining functionality.
dependencies:
dio_flow: ^1.3.3
flutter pub get
import 'package:dio_flow/dio_flow.dart';
For enhanced development experience:
dev_dependencies:
dio_flow: ^1.3.3
# For testing with mocks
mockito: ^5.4.0
# For integration testing
integration_test:
sdk: flutter
import 'package:dio_flow/dio_flow.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// π§ Configure Dio Flow
DioFlowConfig.initialize(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
);
// π Initialize the client
await ApiClient.initialize();
runApp(MyApp());
}
// π‘ Make your first request
final response = await DioRequestHandler.get('users');
if (response is SuccessResponseModel) {
print('β
Success: ${response.data}');
} else {
print('β Error: ${response.error?.message}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// π§ Advanced configuration
DioFlowConfig.initialize(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
// π Enable detailed logging
debugMode: true,
// π Security headers
defaultHeaders: {
'User-Agent': 'MyApp/1.0.0',
'Accept': 'application/json',
},
// π Retry configuration
retryOptions: RetryOptions(
maxAttempts: 3,
retryInterval: const Duration(seconds: 2),
),
);
// π Initialize token management
await TokenManager.initialize();
// π Initialize API 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 is SuccessResponseModel) {
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
Dio Flow provides a comprehensive authentication system with persistent storage, automatic token refresh, and seamless integration:
// Initialize token manager (call this in your main.dart)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await TokenManager.initialize();
// Register refresh logic (consumer-side). Optional β if you don't set it,
// automatic refresh will be disabled and TokenManager will return null on expired token.
TokenManager.setRefreshHandler((refreshToken) async {
// Use DioRequestHandler to call refresh endpoint (consumer controls endpoint details).
final refreshResp = await DioRequestHandler.post(
'auth/refresh',
data: {'refresh_token': refreshToken},
);
if (refreshResp is! SuccessResponseModel) {
throw ApiException('Refresh failed: ${refreshResp.error?.message ?? 'unknown'}');
}
final data = refreshResp.data!;
final expiresIn = (data['expires_in'] as int?) ?? 3600;
return RefreshTokenResponse(
accessToken: data['access_token'] as String,
refreshToken: data['refresh_token'] as String,
expiry: DateTime.now().add(Duration(seconds: expiresIn)),
);
});
runApp(MyApp());
}
// Setting tokens with persistence
await TokenManager.setTokens(
accessToken: 'your_access_token',
refreshToken: 'your_refresh_token',
expiry: DateTime.now().add(Duration(hours: 1)),
);
// Check presence/validity (auto-refresh performed if expired and handler is set)
final hasToken = await TokenManager.hasAccessToken(); // returns true/false
// Getting access token (will automatically refresh if expired and a handler is configured)
final token = await TokenManager.getAccessToken(); // can be null if no handler and expired
// Clearing tokens
await TokenManager.clearTokens();
Key features:
-
Persistent token storage using SharedPreferences (tokens survive app restarts).
-
RefreshTokenHandler typedef β consumer provides a small callback that receives the refresh token and returns a RefreshTokenResponse (access, refresh, expiry). This keeps the package network-agnostic.
-
TokenManager.setRefreshHandler(...) β register refresh logic at startup or runtime (optional).
-
Automatic refresh behavior β hasAccessToken() and getAccessToken() will automatically attempt to refresh when the access token is expired only if a refresh handler has been set. If no handler is provided, expired tokens are treated as absent (methods return false/null).
-Single-flight refresh protection β internal _refreshCompleter ensures only one refresh request runs at a time; concurrent callers wait for that single result.
-
Tokens returned by the refresh handler are persisted via setTokens() (keeps state consistent across restarts).
-
All token operations are asynchronous for non-blocking startup and safer IO.
-
getAccessToken() behavior is unified: it either returns a valid token, triggers refresh (if handler exists), or returns null (if expired and no handler).
// 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),
),
);
void main() async {
await ApiClient.initialize();
// ... rest of your app initialization
}
try {
final response = await DioRequestHandler.get('endpoint');
if (response is SuccessResponseModel) {
// Handle success
} else {
// Use the typed error handling
handleError(response.error);
}
} catch (e) {
// Handle unexpected errors
}
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 is SuccessResponseModel) {
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);
}
}
// πΎ Use caching strategically
final response = await DioRequestHandler.get(
'static-data',
requestOptions: RequestOptionsModel(
shouldCache: true,
cacheMaxAge: const Duration(hours: 1), // Cache static data longer
),
);
// π Batch requests when possible
final futures = [
DioRequestHandler.get('users'),
DioRequestHandler.get('posts'),
DioRequestHandler.get('comments'),
];
final responses = await Future.wait(futures);
// β‘ Use pagination for large datasets
final paginatedResponse = await PaginationUtils.fetchAllPages(
'large-dataset',
parameters: {'per_page': 50}, // Optimal page size
maxPages: 10, // Limit to prevent memory issues
);
// π― Optimize file uploads
final uploadResponse = await FileHandler.uploadBytes(
'upload',
compressedBytes, // Compress before upload
'file.jpg',
onProgress: (sent, total) {
// Update UI efficiently
if (sent % 1024 == 0) { // Update every KB
updateProgressUI(sent / total);
}
},
);
Dio Flow includes a sophisticated mocking system that makes testing your API integrations effortless:
void main() {
// Enable mock mode
MockDioFlow.enableMockMode();
// Register mock responses
MockDioFlow.mockResponse(
'users',
MockResponse.success([
{'id': 1, 'name': 'John Doe'},
{'id': 2, 'name': 'Jane Smith'},
]),
);
// Register response queue for testing pagination
MockDioFlow.mockResponseQueue('posts', [
MockResponse.success({'page': 1, 'data': [...]}),
MockResponse.success({'page': 2, 'data': [...]}),
]);
// Your tests will now use mocked responses
final response = await DioRequestHandler.get('users');
// Disable mock mode
MockDioFlow.disableMockMode();
}
Dio Flow provides first-class GraphQL support with an intuitive query builder and comprehensive operation handling:
// Simple query
final response = await GraphQLHandler.query('''
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
''', variables: {'id': '123'});
// Using query builder
final query = GraphQLQueryBuilder.query()
.operationName('GetUsers')
.variables({'first': 'Int'})
.body('users(first: $first) { id name }')
.build();
// Mutations
final mutationResponse = await GraphQLHandler.mutation('''
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
''', variables: {'input': {'name': 'John', 'email': 'john@example.com'}});
// Batch operations
final operations = [
GraphQLOperation(query: 'query { users { id } }'),
GraphQLOperation(query: 'query { posts { id } }'),
];
final batchResponse = await GraphQLHandler.batch(operations);
Dio Flow provides comprehensive file operations that work consistently across all Flutter platforms, with intelligent platform-specific optimizations:
// File upload from File object
final file = File('/path/to/file.jpg');
final uploadResponse = await FileHandler.uploadFile(
'upload',
file,
fieldName: 'avatar',
additionalData: {'userId': '123'},
onProgress: (sent, total) {
print('Upload: ${(sent/total*100).toStringAsFixed(1)}%');
},
);
// File download to disk
final downloadResponse = await FileHandler.downloadFile(
'files/123/download',
'/local/path/file.pdf',
onProgress: (received, total) {
print('Download: ${(received/total*100).toStringAsFixed(1)}%');
},
);
// Upload from bytes (web-compatible)
final bytesUploadResponse = await FileHandler.uploadBytes(
'upload',
fileBytes, // Uint8List
'filename.jpg',
fieldName: 'avatar',
additionalData: {'userId': '123'},
);
// Download as bytes (web-compatible)
final bytesResponse = await FileHandler.downloadBytes('files/123');
if (bytesResponse is SuccessResponseModel) {
final bytes = bytesResponse.data['bytes'] as Uint8List;
// Use bytes for web download (e.g., trigger browser download)
}
// Multiple file upload (use bytes for web compatibility)
final files = {
'document': documentBytes, // Uint8List for web
'image': imageBytes, // Uint8List for web
};
final multiUploadResponse = await FileHandler.uploadMultipleFiles(
'upload-multiple',
files,
);
// Upload from bytes (works on all platforms)
final bytesUploadResponse = await FileHandler.uploadBytes(
'upload',
fileBytes,
'filename.jpg',
);
Note: On web platform, direct file system access is restricted by browser security. Use
FileHandler.uploadBytes()
andFileHandler.downloadBytes()
for web-compatible file operations.
-
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
-
Mock Issues:
- Ensure
MockDioFlow.enableMockMode()
is called before requests - Verify mock responses are registered for the correct endpoints
- Check that endpoint paths match exactly
- Ensure
-
GraphQL Errors:
- Validate GraphQL syntax in queries
- Check that variables match the schema
- Ensure the GraphQL endpoint is correctly configured
-
File Upload Issues:
- Verify file permissions and paths
- Check server file size limits
- Ensure correct Content-Type headers for multipart uploads
// 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
),
);
// GET request
final users = await DioRequestHandler.get('users');
// POST with data
final created = await DioRequestHandler.post('users', data: userData);
// PUT update
final updated = await DioRequestHandler.put('users/123', data: updates);
// DELETE
final deleted = await DioRequestHandler.delete('users/123');
// With authentication
final profile = await DioRequestHandler.get(
'profile',
requestOptions: RequestOptionsModel(hasBearerToken: true),
);
// With caching
final cached = await DioRequestHandler.get(
'static-data',
requestOptions: RequestOptionsModel(
shouldCache: true,
cacheMaxAge: Duration(minutes: 30),
),
);
// Set tokens
await TokenManager.setTokens(
accessToken: 'access_token',
refreshToken: 'refresh_token',
expiry: DateTime.now().add(Duration(hours: 1)),
);
// Check if authenticated
final isAuthenticated = await TokenManager.hasAccessToken();
// Get current token (auto-refreshes if needed)
final token = await TokenManager.getAccessToken();
// Clear tokens (logout)
await TokenManager.clearTokens();
// Upload file (mobile/desktop)
final upload = await FileHandler.uploadFile('upload', file);
// Upload bytes (all platforms)
final upload = await FileHandler.uploadBytes('upload', bytes, 'file.jpg');
// Download file (mobile/desktop)
final download = await FileHandler.downloadFile('files/123', '/path/file.pdf');
// Download bytes (all platforms)
final download = await FileHandler.downloadBytes('files/123');
// Enable mocking
MockDioFlow.enableMockMode();
// Mock success response
MockDioFlow.mockResponse('users', MockResponse.success([...]));
// Mock error response
MockDioFlow.mockResponse('users', MockResponse.failure('Error message'));
// Mock network error
MockDioFlow.mockResponse('users', MockResponse.networkError());
// Disable mocking
MockDioFlow.disableMockMode();
Contributions are welcome! Whether it's:
- π Bug Reports: Found an issue? Let us know!
- β¨ Feature Requests: Have an idea? We'd love to hear it!
- π Documentation: Help improve our docs
- π§ Code Contributions: Submit a PR!
Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
Thanks to all our amazing contributors who help make Dio Flow better!
This project is licensed under the MIT License - see the LICENSE file for details.
Made with β€οΈ by the Flutter community
Happy coding! π