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:

  1. The http package: 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.
  2. The dio library: 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 MultipartRequest classes.
  • 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:

  1. onRequest: Intercepts the request packet just before it’s sent to the server.
  2. onResponse: Intercepts the response packet right after it’s received from the server, before it’s sent to the code caller.
  3. 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" .-> E

Creating 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 Dimensionhttp Package (Built-in/Official)Dio Library (Third-Party)
Project Size LoadVery Small / MinimalFairly Large / Moderate
Configuration EaseEasy at the start, but verbose at the endRequires initial class setup, but very modular
Middleware Support (Interceptors)None (must build manual wrappers)Yes (built-in onRequest, onResponse, onError)
Request CancellationNot supported by defaultNatively supported via the CancelToken class
Upload/Download ProgressHard to calculate accuratelyVery easy via onSendProgress/onReceiveProgress callbacks
Automatic Re-authenticationMust be written manually in every API hitVery easy to manage using QueuedInterceptor
Automatic JSON CastingNone (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 http package 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.
  • CancelToken allows you to cancel running async requests to save internet quota and user device battery life.
  • FormData is used to send multipart multimedia files (like photos or documents) complete with upload progress monitoring.
  • DioException divides network failure types specifically to make it easier to display user-friendly error messages based on the disruption type.
  • Needs Evaluation: Choose http for small/minimal projects, and choose Dio for production-scale app architecture.

← Previous: Overview   Next: JSON & Serialization →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact