Flutter  

How to Handle API Calls in Flutter Using Dio Package Step-by-Step

Introduction

When building modern mobile applications using Flutter, interacting with APIs is a core requirement. Whether you're fetching user data, submitting forms, or integrating third-party services, efficient API handling becomes critical for performance and user experience. One of the most powerful and flexible libraries for handling HTTP requests in Flutter is Dio. Compared to the basic HTTP package, Dio offers advanced features like interceptors, global configuration, request cancellation, file downloading, and more.

In this guide, you’ll learn step-by-step how to handle API calls in Flutter using Dio, explained in simple and practical terms with real-world examples.

What is Dio in Flutter?

Dio is a third-party HTTP client for Flutter that simplifies network requests and provides advanced capabilities like:

  • Interceptors (for logging, authentication, etc.)

  • Global configuration

  • FormData support

  • Request cancellation

  • Timeout handling

  • File upload/download

Real-life analogy: Think of Dio as a smart delivery manager. Instead of just sending a parcel (API request), it tracks, logs, retries, and manages everything efficiently.

Step 1: Add Dio Dependency

First, you need to include Dio in your Flutter project.

Add this to your pubspec.yaml file:

dependencies:
  dio: ^5.0.0

Then run:

flutter pub get

Step 2: Import Dio Package

Now import Dio in your Dart file where API calls will be handled:

import 'package:dio/dio.dart';

Step 3: Create a Dio Instance

Create a reusable Dio instance for better scalability.

final Dio dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 5),
    receiveTimeout: const Duration(seconds: 3),
  ),
);

Real-world example: Instead of setting address and rules every time you order something, you store them once.

Step 4: Make a GET API Call

Future<void> fetchUsers() async {
  try {
    final response = await dio.get('/users');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

Before vs After:

  • Before: Manual request handling, limited control

  • After: Structured, scalable, and easy-to-manage API calls

Step 5: Make a POST API Call

Future<void> createUser() async {
  try {
    final response = await dio.post(
      '/users',
      data: {
        'name': 'John',
        'email': '[email protected]',
      },
    );
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

Real-life example: Submitting a form in an app like a signup page.

Step 6: Add Interceptors (Very Important)

Interceptors help you log requests, add tokens, and handle errors globally.

dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      options.headers['Authorization'] = 'Bearer YOUR_TOKEN';
      return handler.next(options);
    },
    onResponse: (response, handler) {
      return handler.next(response);
    },
    onError: (DioException e, handler) {
      print('Error: ${e.message}');
      return handler.next(e);
    },
  ),
);

Real-world example: Like a security guard checking ID before entering a building.

Step 7: Handle Errors Properly

try {
  final response = await dio.get('/users');
} on DioException catch (e) {
  if (e.type == DioExceptionType.connectionTimeout) {
    print('Connection Timeout');
  } else if (e.type == DioExceptionType.badResponse) {
    print('Server Error');
  } else {
    print('Unexpected Error');
  }
}

User-visible symptoms if ignored:

  • App crashes

  • No error messages

  • Poor user experience

Step 8: Create a Service Class (Best Practice)

class ApiService {
  final Dio _dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

  Future<Response> getUsers() async {
    return await _dio.get('/users');
  }
}

This keeps your code clean and maintainable.

Step 9: Use API in UI (Example)

FutureBuilder(
  future: fetchUsers(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    return Text('Data Loaded');
  },
)

Advantages of Using Dio

  • Better error handling

  • Clean and scalable architecture

  • Supports interceptors and middleware

  • Easy debugging with logs

  • Timeout and retry support

Disadvantages of Using Dio

  • Slight learning curve for beginners

  • More setup compared to basic HTTP

  • Overkill for very small apps

Common Mistakes to Avoid

  • Not handling exceptions properly

  • Hardcoding API URLs everywhere

  • Ignoring interceptors

  • Blocking UI during API calls

Summary

Handling API calls in Flutter using Dio makes your application more structured, scalable, and production-ready. By setting up a centralized Dio instance, using interceptors for authentication and logging, and properly managing errors, you can significantly improve both developer experience and app performance. In real-world scenarios, this approach helps avoid crashes, ensures smooth data flow, and makes your code easier to maintain as your application grows.