Dio & HTTP #
When building a Flutter app that needs interaction with a backend server, one of the most fundamental decisions you must make is choosing the HTTP client to use. The HTTP client is responsible for assembling the request data packets, sending them across the internet, and receiving responses back for the app to process.
In the Dart and Flutter development ecosystem, there are two main choices for handling HTTP clients:
- The
httppackage: The official library developed directly by the Dart team. It’s very lightweight, minimal, and well-suited for simple data-sending needs or fast prototyping. - The
diolibrary: A very popular and powerful third-party library. Dio is designed to meet production-scale app needs requiring advanced configuration like automatic interceptor handling, request cancellation, multipart file uploads, centralized network logging, and automatic connection retries (auto retry).
In this document, we’ll discuss in depth how to use both libraries, compare each one’s advantages, learn the singleton client design pattern, configure interceptors as middleware, and build a robust error handling strategy to improve your app’s user experience.
The http Package — Simple and Concise #
The http package is Dart’s official library designed with a simplicity philosophy. This library doesn’t have many extra features beyond the basic HTTP data-sending functions, making it very easy to understand even for beginner developers.
To use it, first add the http dependency to your project’s pubspec.yaml file:
dependencies:
http: ^1.2.2
Implementing CRUD Operations with the http Library #
Here’s a complete example of creating an API client class for managing product data using the http library. We use basic functions like get, post, put, and delete.
import 'dart:convert';
import 'package:http/http.dart' as http;
class HttpProductService {
static const String _baseUrl = 'https://api.ourstore.com/v1';
// Standard headers for JSON communication
final Map<String, String> _headers = {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
};
// 1. GET - Fetch the product list
Future<List<Map<String, dynamic>>> fetchAllProducts() async {
try {
final response = await http.get(
Uri.parse('$_baseUrl/products'),
headers: _headers,
);
if (response.statusCode == 200) {
final List<dynamic> rawData = jsonDecode(response.body);
return rawData.map((item) => item as Map<String, dynamic>).toList();
} else {
throw Exception('Failed to fetch products. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Network error occurred: $e');
}
}
// 2. POST - Create a new product
Future<Map<String, dynamic>> createNewProduct(Map<String, dynamic> product) async {
try {
final response = await http.post(
Uri.parse('$_baseUrl/products'),
headers: _headers,
body: jsonEncode(product),
);
if (response.statusCode == 201) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
throw Exception('Failed to create new product. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Network error occurred: $e');
}
}
// 3. PUT - Update a product completely
Future<Map<String, dynamic>> updateProduct(String id, Map<String, dynamic> product) async {
try {
final response = await http.put(
Uri.parse('$_baseUrl/products/$id'),
headers: _headers,
body: jsonEncode(product),
);
if (response.statusCode == 200) {
return jsonDecode(response.body) as Map<String, dynamic>;
} else {
throw Exception('Failed to update product. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Network error occurred: $e');
}
}
// 4. DELETE - Delete a product
Future<void> deleteProduct(String id) async {
try {
final response = await http.delete(
Uri.parse('$_baseUrl/products/$id'),
headers: _headers,
);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception('Failed to delete product. Status: ${response.statusCode}');
}
} catch (e) {
throw Exception('Network error occurred: $e');
}
}
}
Main Limitations of the http Package #
Although writing code with http feels very fast, you’ll start facing serious obstacles when your app grows and needs a more dynamic network architecture. Some limitations of the http library include:
- No Global Configuration: You have to rewrite the base URL and headers manually in every request function. If the base URL changes, you have to change it in many places or design an exhausting custom wrapper.
- No Built-in Interceptors: You can’t automatically inject authentication tokens into all requests or handle expired token cases centrally.
- Complicated File Uploads: Sending multipart data (like user profile photos) requires writing verbose
MultipartRequestclasses. - No Cancellation Feature: You can’t cancel a running request when the user suddenly navigates out of the screen, which can waste device memory and performance.
The Dio Library — A Robust HTTP Client for Production #
To overcome the limitations above, the Flutter community recommends using the Dio library. Dio offers all the advanced features modern mobile apps need to communicate efficiently with API servers.
To start using Dio, register its dependency in your pubspec.yaml file:
dependencies:
dio: ^5.7.0
Configuring Dio with BaseOptions #
In Dio, you can configure network settings centrally when first initializing the Dio instance using the BaseOptions object.
import 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
// Base URL automatically prepended to request paths
baseUrl: 'https://api.ourstore.com/v1',
// Timeout tolerance for opening the initial connection
connectTimeout: const Duration(seconds: 10),
// Timeout tolerance for receiving data responses from the server
receiveTimeout: const Duration(seconds: 15),
// Timeout for sending data from the device to the server
sendTimeout: const Duration(seconds: 15),
// Global headers always injected into every request
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
Implementing a Singleton for the Dio Instance #
In a healthy Flutter app architecture, you must not create a new Dio instance in every API class. Creating Dio instances repeatedly will waste RAM memory and can break the TCP connection pool optimized by the operating system.
You must use the Singleton Pattern to ensure there’s only one Dio instance alive in memory during your app’s lifecycle.
Here’s a safe, production-ready singleton-based network client class design:
// lib/core/network/api_client.dart
import 'package:dio/dio.dart';
class ApiClient {
// 1. Store the private static instance
static final ApiClient _instance = ApiClient._internal();
// 2. Factory constructor returning the same instance
factory ApiClient() => _instance;
// Container variable for the Dio instance
late final Dio dio;
// 3. Internal named constructor for one-time initialization
ApiClient._internal() {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.ourstore.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
// Here we can add our global Interceptors later
dio.interceptors.addAll([
LoggyInterceptor(), // Example logging interceptor
]);
}
}
// Usage throughout your app:
// final dio = ApiClient().dio;
Complete CRUD Operations with Dio #
Once you have the global configuration and singleton, writing CRUD code with Dio becomes much more concise. Dio automatically parses JSON format in the background, so you don’t need to call the jsonDecode function manually.
class DioProductService {
final Dio _dio;
// Dependency injection via constructor
DioProductService(this._dio);
// 1. GET with dynamic query parameters (e.g., for search & pagination)
Future<List<dynamic>> fetchProducts({
int page = 1,
int limit = 20,
String? search,
}) async {
final response = await _dio.get(
'/products',
queryParameters: {
'page': page,
'limit': limit,
if (search != null) 'search': search,
},
);
// Dio automatically converts the response body to Dart Map/List data types
return response.data as List<dynamic>;
}
// 2. POST with the JSON body payload sent directly
Future<Map<String, dynamic>> addProduct(Map<String, dynamic> productData) async {
final response = await _dio.post(
'/products',
data: productData, // Just pass the Map, Dio auto-encodes it to JSON
);
return response.data as Map<String, dynamic>;
}
// 3. PUT for total data updates
Future<Map<String, dynamic>> updateProduct(String id, Map<String, dynamic> newData) async {
final response = await _dio.put(
'/products/$id',
data: newData,
);
return response.data as Map<String, dynamic>;
}
// 4. PATCH for partial field updates
Future<Map<String, dynamic>> updateProductPartially(String id, Map<String, dynamic> changedFields) async {
final response = await _dio.patch(
'/products/$id',
data: changedFields,
);
return response.data as Map<String, dynamic>;
}
// 5. DELETE to remove data
Future<void> deleteProduct(String id) async {
await _dio.delete('/products/$id');
}
}
Interceptors — Client Network Middleware #
One of Dio’s biggest advantages is its support for Interceptors. Interceptors act like middleware on network traffic. Interceptors have three main gates:
onRequest: Intercepts the request packet just before it’s sent to the server.onResponse: Intercepts the response packet right after it’s received from the server, before it’s sent to the code caller.onError: Intercepts network errors before the error is thrown to the app UI.
Here’s an Interceptor workflow diagram acting as a checkpoint for your data traffic:
graph TD
classDef default stroke:#333,stroke-width:2px;
A["HTTP Client Call (App)"] -->|"1. Trigger Request"| B["Request Interceptor"]
B -->|"2. Add Token / Headers"| C["Internet (API Server)"]
C -->|"3. Send Response"| D["Response Interceptor"]
D -->|"4. Log / Modify Data"| E["UI / Client Repository"]
C -. "4a. If Error (401/Timeout)" .-> F["Error Interceptor"]
F -. "4b. Refresh Token / Retry Request" .-> B
F -. "4c. Forward Error if Failed" .-> ECreating a Custom Authentication Interceptor #
Let’s create a custom Interceptor responsible for automatically injecting the JWT security token into every request from secure storage, so you don’t need to write the authorization header in every API function repeatedly.
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 1. Get the JWT access token from secure local storage
final accessToken = LocalStorage.getAccessToken();
if (accessToken != null) {
// 2. Automatically attach it to the Authorization header
options.headers['Authorization'] = 'Bearer $accessToken';
}
// 3. Continue the request journey to the server
super.onRequest(options, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
// Here we can detect if a 401 (Unauthorized) error occurs.
// We can trigger an automatic token refresh process (covered in the Authentication module).
if (err.response?.statusCode == 401) {
print('Token expired, token refresh needed.');
}
super.onError(err, handler);
}
}
CancelToken — Stopping Unneeded Requests #
Imagine the following scenario: the user is on the Product Search Page, typing keywords quickly, triggering 5 consecutive search requests to the API server. Before the fifth request finishes, the user suddenly presses the Back button to leave the screen.
If we don’t cancel those requests, the app will keep processing network data in the background pointlessly. Dio provides the CancelToken class to solve this efficiency problem.
Here’s the dynamic search request cancellation implementation:
class ProductSearchService {
final Dio _dio;
CancelToken? _cancelToken;
ProductSearchService(this._dio);
Future<List<dynamic>> searchProducts(String query) async {
// 1. If there's a previous search request still running, cancel it immediately!
if (_cancelToken != null && !_cancelToken!.isCancelled) {
_cancelToken!.cancel('A new search was triggered by the user.');
}
// 2. Create a new CancelToken instance
_cancelToken = CancelToken();
try {
final response = await _dio.get(
'/products/search',
queryParameters: {'q': query},
cancelToken: _cancelToken, // 3. Attach the cancellation token to the request
);
return response.data as List<dynamic>;
} on DioException catch (e) {
// 4. Detect whether the error was caused by deliberate cancellation
if (CancelToken.isCancel(e)) {
print('Request successfully cancelled: ${e.message}');
return []; // Return an empty array, this isn't a real network error
}
rethrow; // If it's a real network error, throw it upward
}
}
}
Uploading Files with FormData #
Sending multimedia files like images, videos, or PDF documents from a Flutter app to an API server requires a special request format called multipart/form-data. Dio provides the FormData class to assemble multipart data very easily.
You can also monitor the file upload progress percentage to display as a ProgressBar indicator in your app’s user interface using the onSendProgress callback.
import 'dart:io';
import 'package:dio/dio.dart';
class FileUploadService {
final Dio _dio;
FileUploadService(this._dio);
Future<String> uploadProfilePhoto(File imageFile, String userId) async {
// 1. Extract the file name from the local path
final fileName = imageFile.path.split('/').last;
// 2. Assemble the FormData object (similar to HTML form format)
final formData = FormData.fromMap({
'user_id': userId,
// Read the physical file into a MultipartFile asynchronously
'photo': await MultipartFile.fromFile(
imageFile.path,
filename: fileName,
),
});
try {
final response = await _dio.post(
'/user/upload-photo',
data: formData,
// Provide the byte data upload progress callback
onSendProgress: (int sentBytes, int totalBytes) {
if (totalBytes != -1) {
final double percentage = (sentBytes / totalBytes) * 100;
print('Upload Progress: ${percentage.toStringAsFixed(0)}%');
}
},
);
// Return the uploaded photo URL given by the server
return response.data['photo_url'] as String;
} catch (e) {
throw Exception('Failed to upload profile photo: $e');
}
}
}
Downloading Files with a Progress Bar #
Besides uploading, Dio also provides a ready-made download() function for downloading large binary files (like financial report PDFs or ZIP archive files) from the internet directly to the device’s local directory.
Just like the upload process, you can monitor download progress using the onReceiveProgress callback.
class FileDownloadService {
final Dio _dio;
FileDownloadService(this._dio);
Future<void> downloadEbook({
required String ebookUrl,
required String localStoragePath,
required void Function(double progress) updateUiProgress,
}) async {
try {
await _dio.download(
ebookUrl,
localStoragePath,
// Monitor the byte data received from the server
onReceiveProgress: (int receivedBytes, int totalBytes) {
if (totalBytes != -1) {
final double progressRatio = receivedBytes / totalBytes;
// Send the progress value (0.0 to 1.0) to the UI update function
updateUiProgress(progressRatio);
}
},
options: Options(
responseType: ResponseType.bytes, // Receive data as binary bytes
followRedirects: true,
),
);
} catch (e) {
throw Exception('The file download process failed: $e');
}
}
}
Structured Error Handling with DioException #
The internet is very unpredictable. Sometimes the Wi-Fi connection cuts out mid-way, the backend server crashes, or data packets time out due to bad weather. To ensure your app stays stable, you must categorize network failure types structurally using the DioException class.
Let’s create a safe request wrapper function to map Dio’s technical errors into custom Exception objects that are friendly for ordinary users:
// Defining Our Custom Exception Class
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
@override
String toString() => message;
}
class ApiService {
final Dio _dio;
ApiService(this._dio);
// Safe wrapper for executing our network requests
Future<T> safeExecute<T>(Future<Response<T>> Function() request) async {
try {
final response = await request();
return response.data as T;
} on DioException catch (e) {
// Mapping DioException categories
switch (e.type) {
case DioExceptionType.connectionTimeout:
throw NetworkException('Connection timed out. Please check your internet signal.');
case DioExceptionType.sendTimeout:
throw NetworkException('Failed to send data to the server on time.');
case DioExceptionType.receiveTimeout:
throw NetworkException('The server took too long to respond to your request.');
case DioExceptionType.connectionError:
throw NetworkException('Your device is not connected to the internet.');
case DioExceptionType.badResponse:
// Happens when the server replies with a failure status code (4xx or 5xx)
final statusCode = e.response?.statusCode;
final errorData = e.response?.data;
final serverErrorMessage = errorData is Map
? errorData['message'] ?? 'A system error occurred'
: 'Unknown error';
switch (statusCode) {
case 400:
throw NetworkException('The sent data format is incorrect: $serverErrorMessage');
case 401:
throw NetworkException('Your login session has expired. Please sign in again.');
case 403:
throw NetworkException('You don\'t have access to open this data.');
case 404:
throw NetworkException('The data you\'re looking for wasn\'t found on the server.');
case 422:
throw NetworkException('Data validation failed: $serverErrorMessage');
case 500:
throw NetworkException('The internal server is experiencing issues. Please try again in a moment.');
default:
throw NetworkException('Server Issue (Code: $statusCode): $serverErrorMessage');
}
case DioExceptionType.cancel:
throw NetworkException('The network request was cancelled by the system.');
default:
throw NetworkException('An unknown network issue occurred.');
}
} catch (e) {
// Catching non-DioException errors (like data type casting errors)
throw NetworkException('Failed to process data: $e');
}
}
}
Head-to-Head Comparison: http vs Dio #
To make it easier for you and your team to determine which library is right for your project, here’s a summary comparison of each library’s strengths and weaknesses:
| Feature Dimension | http Package (Built-in/Official) | Dio Library (Third-Party) |
|---|---|---|
| Project Size Load | Very Small / Minimal | Fairly Large / Moderate |
| Configuration Ease | Easy at the start, but verbose at the end | Requires initial class setup, but very modular |
| Middleware Support (Interceptors) | None (must build manual wrappers) | Yes (built-in onRequest, onResponse, onError) |
| Request Cancellation | Not supported by default | Natively supported via the CancelToken class |
| Upload/Download Progress | Hard to calculate accurately | Very easy via onSendProgress/onReceiveProgress callbacks |
| Automatic Re-authentication | Must be written manually in every API hit | Very easy to manage using QueuedInterceptor |
| Automatic JSON Casting | None (must jsonDecode manually) | Automatically parsed by Dio’s internal parser |
In general, use the http package if you’re building a simple hobby app, a fast app prototype, or a pure Dart library that must not depend on external third-party libraries. Use the Dio library as the main standard for building production-scale commercial apps requiring stability, high efficiency, complex security token handling, and high architectural scalability.
Summary #
- The
httppackage is officially developed by the Dart team with a minimalist approach, well-suited for simple request processing or fast prototyping.- The Dio library is the most popular Flutter HTTP client library for production thanks to its complete feature set like interceptors, request cancellation, and progress tracking.
- The Singleton Pattern must be applied to the DioClient instance to save device RAM allocation and maximize efficient TCP pool usage.
- Interceptors act as centralized security checkpoints (middleware) for manipulating requests (injecting tokens) or handling errors globally.
CancelTokenallows you to cancel running async requests to save internet quota and user device battery life.FormDatais used to send multipart multimedia files (like photos or documents) complete with upload progress monitoring.DioExceptiondivides network failure types specifically to make it easier to display user-friendly error messages based on the disruption type.- Needs Evaluation: Choose
httpfor small/minimal projects, and chooseDiofor production-scale app architecture.