Lesson 43 of 51 intermediate

Dio HTTP Client

Professional Networking for Flutter

Open interactive version (quiz + challenge)

Real-world analogy

If the basic http package is like sending letters by regular mail, Dio is like having a personal assistant who handles all your mail. Your assistant (Dio) adds your return address to every letter (base URL), stamps them all (auth headers via interceptors), keeps copies (logging), retries if the mailman loses one (retry interceptor), and even opens the replies for you (response transformation)!

What is it?

Dio is a powerful HTTP client for Dart used as the networking layer in team_mvp_kit. It provides interceptors for automatic token injection and refresh, configurable timeouts, request cancellation, detailed error typing, logging, and a clean API for all HTTP methods. Every request flows through a middleware pipeline of interceptors.

Real-world relevance

In team_mvp_kit, Dio is configured once in the DI module with base URL, timeouts, and interceptors. The AuthInterceptor attaches tokens and refreshes expired ones. The ApiService wraps Dio with typed methods so repositories never touch raw HTTP. Every API call flows through: Dio -> AuthInterceptor -> LogInterceptor -> Server -> Response.

Key points

Code example

import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';

// 1. Dio Module - configured once, injected everywhere
@module
abstract class NetworkModule {
  @singleton
  Dio dio(TokenStorage tokenStorage) {
    final dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com/v1',
      connectTimeout: const Duration(seconds: 15),
      receiveTimeout: const Duration(seconds: 15),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    ));

    dio.interceptors.addAll([
      AuthInterceptor(tokenStorage, dio),
      LogInterceptor(
        requestBody: true,
        responseBody: true,
        logPrint: (msg) => debugPrint(msg.toString()),
      ),
    ]);

    return dio;
  }
}

// 2. Auth Interceptor - auto-attaches and refreshes tokens
class AuthInterceptor extends Interceptor {
  final TokenStorage _tokenStorage;
  final Dio _dio;

  AuthInterceptor(this._tokenStorage, this._dio);

  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) {
    final token = _tokenStorage.accessToken;
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onError(
    DioException error,
    ErrorInterceptorHandler handler,
  ) async {
    if (error.response?.statusCode == 401) {
      final refreshed = await _tokenStorage.refreshTokens();
      if (refreshed) {
        error.requestOptions.headers['Authorization'] =
            'Bearer ${_tokenStorage.accessToken}';
        final response = await _dio.fetch(error.requestOptions);
        return handler.resolve(response);
      }
    }
    handler.next(error);
  }
}

// 3. ApiService - typed wrapper used by repositories
@lazySingleton
class ApiService {
  final Dio _dio;
  ApiService(this._dio);

  Future<Map<String, dynamic>> get(
    String path, {
    Map<String, dynamic>? queryParams,
  }) async {
    final response = await _dio.get(
      path, queryParameters: queryParams,
    );
    return response.data as Map<String, dynamic>;
  }

  Future<Map<String, dynamic>> post(
    String path, {
    Map<String, dynamic>? data,
  }) async {
    final response = await _dio.post(path, data: data);
    return response.data as Map<String, dynamic>;
  }
}

Line-by-line walkthrough

  1. 1. Import the Dio package
  2. 2. Import injectable for dependency injection annotations
  3. 3.
  4. 4. Comment: Dio is configured once in a module
  5. 5. @module annotation for registering third-party classes
  6. 6. Abstract class for the network module definition
  7. 7. @singleton ensures one Dio instance for the whole app
  8. 8. Method that creates and configures Dio, receiving TokenStorage
  9. 9. Create a new Dio instance with BaseOptions
  10. 10. Set the base URL that is prepended to all request paths
  11. 11. Set connection timeout to 15 seconds
  12. 12. Set receive timeout to 15 seconds
  13. 13. Opening default headers map
  14. 14. All requests send JSON content type
  15. 15. All requests accept JSON responses
  16. 16. Closing headers and BaseOptions
  17. 17.
  18. 18. Add both interceptors to Dio's pipeline
  19. 19. AuthInterceptor handles token injection and refresh
  20. 20. LogInterceptor prints requests and responses for debugging
  21. 21. Closing the addAll call
  22. 22.
  23. 23. Return the fully configured Dio instance
  24. 24. Closing the dio method and NetworkModule
  25. 25.
  26. 26. Comment: AuthInterceptor handles authentication automatically
  27. 27. Class extending Dio's Interceptor base class
  28. 28. Private field for token storage
  29. 29. Private field for Dio (needed for retry requests)
  30. 30.
  31. 31. Constructor receives both dependencies
  32. 32.
  33. 33. Override onRequest which runs before every outgoing request
  34. 34. Method signature with RequestOptions and handler
  35. 35. Get the current access token from storage
  36. 36. If we have a token, add it to the Authorization header
  37. 37. Setting the Bearer token header value
  38. 38. Closing the if block
  39. 39. Continue to the next interceptor or send the request
  40. 40. Closing onRequest
  41. 41.
  42. 42. Override onError which runs on every failed response
  43. 43. Method signature with DioException and handler
  44. 44. Check if the error was a 401 Unauthorized response
  45. 45. Try to refresh the expired token via token storage
  46. 46. If refresh succeeded, update the header on the original request
  47. 47. Set the new token value
  48. 48. Retry the original request using Dio fetch
  49. 49. Return the successful retry response to the caller
  50. 50. Closing the refresh success and 401 check blocks
  51. 51. If not 401 or refresh failed, pass the error along
  52. 52. Closing onError and AuthInterceptor class
  53. 53.
  54. 54. Comment: ApiService is the typed wrapper used by repositories
  55. 55. @lazySingleton means one instance, created on first use
  56. 56. ApiService class declaration
  57. 57. Private Dio field for making requests
  58. 58. Constructor receives Dio via injection
  59. 59.
  60. 60. GET method returns a parsed JSON map
  61. 61. Takes a path and optional query parameters
  62. 62. Send GET request via Dio with query parameters
  63. 63. Closing the get call
  64. 64. Cast and return response data as Map
  65. 65. Closing get method
  66. 66.
  67. 67. POST method returns a parsed JSON map
  68. 68. Takes a path and optional data body
  69. 69. Send POST request via Dio with data
  70. 70. Cast and return response data as Map
  71. 71. Closing post method and ApiService class

Spot the bug

final dio = Dio(BaseOptions(
  baseUrl: 'https://api.example.com',
));

final response = await dio.get('https://api.example.com/users');
print(response.data);
Need a hint?
Look at the URL being passed to dio.get when a baseUrl is already configured...
Show answer
When baseUrl is set, dio.get() expects a relative path like '/users', not the full URL. Passing the full URL causes Dio to use it as-is, making baseUrl pointless. Worse, if the domains differ you hit the wrong server. Fix: change to dio.get('/users').

Explain like I'm 5

Imagine you send lots of letters to different people. With regular mail, you write your address, stick a stamp, and seal every single letter yourself. Dio is like hiring a postal assistant who does all that automatically. You just say 'send this to Bob' and the assistant adds your address, stamps it, logs it, and if the letter gets lost, sends it again without you even knowing!

Fun fact

Dio is the most popular HTTP package in the Flutter ecosystem with over 12,000 GitHub stars. The name comes from the Italian word for 'God' -- fitting because it handles networking with seemingly divine power compared to the basic http package!

Hands-on challenge

Create a Dio instance with BaseOptions (base URL, 10-second timeout). Add an InterceptorsWrapper that logs every request URL and every response status code. Make a GET request to https://jsonplaceholder.typicode.com/posts/1 and print the title from the response.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart