All Guides
    API & Data
    API
    Dio
    HTTP
    Advanced Networking

    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 dio package to your project's pubspec.yaml dependencies:
      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:

    1. Singleton Pattern: The private constructor ApiService._internal ensures that only one instance of the client is created and reused throughout the app's lifecycle, preserving socket connections and caching resources.
    2. Request Interceptor: Automatically appends an Authorization header to every outgoing request.
    3. Automatic JSON Decoding: Unlike the standard http package, Dio automatically decodes JSON response bodies into Dart Maps or Lists, eliminating the need to call json.decode(response.body) manually.
    4. _handleDioError Helper: Categorizes DioExceptionType values 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 DioException specifically 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 BaseOptions to 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:


    Summary

    • Dio is an advanced HTTP client featuring base options, interceptors, and robust error handling.
    • BaseOptions configures 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

    1. Previous Guide: Fetching API Data in Flutter
    2. Next Guide: Local Storage with Hive
    3. 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