Using Dio for HTTP Requests
Published April 2026
Introduction
While the standard http package is excellent for basic network requests, production applications often require advanced networking features. As your app scales, you will need to handle global requirements like appending authorization tokens to every request, logging network traffic for debugging, automatically retrying failed requests, setting timeout limits, and handling file uploads with upload progress tracking.
Implementing these features manually using the standard http package requires writing complex boilerplate wrappers. To simplify this, Flutter developers use Dio, a powerful Http client for Dart.
Dio provides built-in support for request/response interceptors, global base configurations, form data file uploads, request cancellation, timeout limits, and robust exception handling. In this guide, you will learn the advantages of Dio, configure a production-ready API service client with request interceptors, and master custom error handling.
Prerequisites
Before implementing Dio, ensure you have:
- Read our Fetching API Data in Flutter guide.
- A solid grasp of Dart asynchronous programming (
async,await,Future). - Added the
diopackage to your project'spubspec.yamldependencies:dependencies: dio: ^5.4.0
Core Explanation: Advanced Networking with Dio
Dio simplifies networking by configuring global parameters and intercepting requests before they reach the server.
Flutter App Interceptors Flow REST API
┌─────────────┐ ┌──────────────────┐ ┌─────────┐
│ │ │ onRequest() ├────►│ │
│ Request ├────►│ Append Auth Token│ │ Web │
│ │ └──────────────────┘ │ Server │
│ │ ┌──────────────────┐ │ │
│ Response │◄────┤ onResponse() │◄────┤ │
│ │ │ Logger / Caching │ └─────────┘
└─────────────┘ └──────────────────┘
1. BaseOptions (Global Client Settings)
Instead of specifying configurations (like host URLs, headers, and timeouts) on every request, you define them once globally using BaseOptions.
final options = BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
headers: {
'Accept': 'application/json',
},
);
final dio = Dio(options);
2. Interceptors (Request/Response Hooks)
Interceptors are middleware hooks that let you intercept and modify requests, responses, and errors before they are handled by the main application logic:
onRequest: Runs before the request is sent to the server. Ideal for automatically appending authorization headers.onResponse: Runs when the response returns. Ideal for global logging or caching.onError: Runs when a request fails. Ideal for refreshing expired tokens or showing global error banners.
3. Sane Exception Handling: DioException
When a request fails, Dio throws a DioException object containing detailed error metadata, such as:
DioExceptionType.connectionTimeout: The connection timed out.DioExceptionType.badResponse: The server returned an error code (e.g. 401, 404, 500).DioExceptionType.cancel: The request was cancelled.
Practical Example: Composing an API Service Singleton
Let's build a clean, compiler-ready API Client service class that uses a singleton pattern and implements interceptors for global logging and authorization header injection.
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
// ==========================================
// API CLIENT SINGLETON WRAPPER
// ==========================================
class ApiService {
// 1. Private constructor for the Singleton pattern
ApiService._internal() {
_initializeDio();
}
// 2. The single active instance of our service class
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
late final Dio _dio;
// Getter to expose the configured dio instance if needed
Dio get client => _dio;
void _initializeDio() {
final options = BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com',
connectTimeout: const Duration(seconds: 8), // 8 seconds limit
receiveTimeout: const Duration(seconds: 5), // 5 seconds limit
headers: {
'Content-Type': 'application/json',
},
);
_dio = Dio(options);
// 3. Injecting Request and Error Interceptors
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Append mock authorization token headers dynamically
const mockAuthToken = 'Bearer custom_jwt_token_here';
options.headers['Authorization'] = mockAuthToken;
if (kDebugMode) {
print('--> SENDING REQUEST: ${options.method} ${options.uri}');
}
return handler.next(options); // Continue request execution
},
onResponse: (response, handler) {
if (kDebugMode) {
print('<-- RESPONSE RECEIVED: ${response.statusCode} from ${response.requestOptions.path}');
}
return handler.next(response); // Continue response execution
},
onError: (DioException error, handler) {
// Global error logging
if (kDebugMode) {
print('[API ERROR] ${error.type}: ${error.message}');
}
// Handle specific response errors
if (error.type == DioExceptionType.badResponse) {
final statusCode = error.response?.statusCode;
if (statusCode == 401) {
print('Redirect to login page...');
}
}
return handler.next(error); // Continue error propagation
},
),
);
}
// ==========================================
// API REQUEST METHODS
// ==========================================
/// Fetches details from a specific API endpoint.
Future<Map<String, dynamic>> fetchPost(int postId) async {
try {
final response = await _dio.get('/posts/$postId');
// Dio automatically decodes JSON responses into Map/List objects
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
// Parse error types cleanly
throw _handleDioError(e);
}
}
String _handleDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
return 'Connection timeout. Check your internet connection.';
case DioExceptionType.receiveTimeout:
return 'Server took too long to respond.';
case DioExceptionType.badResponse:
final statusCode = error.response?.statusCode;
return 'Server returned error code: $statusCode';
case DioExceptionType.connectionError:
return 'Internet connection failed.';
default:
return 'An unexpected network error occurred.';
}
}
}
Code Explanation:
- Singleton Pattern: The private constructor
ApiService._internalensures that only one instance of the client is created and reused throughout the app's lifecycle, preserving socket connections and caching resources. - Request Interceptor: Automatically appends an
Authorizationheader to every outgoing request. - Automatic JSON Decoding: Unlike the standard
httppackage, Dio automatically decodes JSON response bodies into Dart Maps or Lists, eliminating the need to calljson.decode(response.body)manually. _handleDioErrorHelper: CategorizesDioExceptionTypevalues to return clean, user-friendly error messages.
Common Mistakes
1. Hardcoding Headers on Every Request
Manually appending headers (like 'Authorization': 'token') to every API call creates duplicate code and makes updating token logic difficult.
- The Fix: Use a request interceptor to append authorization headers automatically.
2. Catching Generic Exceptions Instead of DioException
Catching generic Exception objects instead of DioException prevents you from extracting useful network metadata (like status codes or timeout types).
- The Fix: Always catch
DioExceptionspecifically when writing catch wrappers.
3. Creating Multiple Client Instances
Instantiating a new Dio client inside every request function wastes network sockets and breaks interceptor configurations.
- The Fix: Use a Singleton wrapper pattern to ensure a single instance is shared across the app.
Best Practices
- Implement Network Timeouts: Always set connection and read timeouts on
BaseOptionsto prevent requests from hanging indefinitely on poor network connections. - Use Singletons: Wrap your Dio client in a singleton class to reuse connections and configuration settings.
- Log Responsibly: Use interceptors to log request and response metadata in development, but disable console logging in production builds.
Related Concepts
Continue mastering network architecture in Flutter by exploring these guides:
- Fetching API Data in Flutter: Review basic HTTP request and FutureBuilder state logic.
- Managing State with Provider: Learn how to fetch API data within state classes and bind it to UI elements.
- Performance Optimization Tips: Explore network caching strategies to speed up your application.
Summary
- Dio is an advanced HTTP client featuring base options, interceptors, and robust error handling.
BaseOptionsconfigures base URLs, timeout durations, and headers globally.- Interceptors act as middleware hooks to modify requests, responses, and errors.
- Use a Singleton wrapper pattern to configure and reuse a single client instance.
Next Steps
- Previous Guide: Fetching API Data in Flutter
- Next Guide: Local Storage with Hive
- Related Guide: Managing State with Provider
Ready to take the next step?
Start our free Flutter course and build real apps today.
Start Learning Now