Mastering HTTP Requests with Dio in Flutter

Mastering HTTP Requests with Dio in Flutter

What You’ll Learn

Learn how to use Dio, Flutter’s powerful HTTP client, for advanced networking. While the basic http package works for simple requests, Dio provides interceptors, request cancellation, file uploads, and better error handling—essential for production apps.

Why Choose Dio Over http?

Dio offers features that make API integration easier:

Add Dio to your pubspec.yaml:

dependencies:
  dio: ^5.4.0

Example: REST API Client with Error Handling

Here’s a practical API service class using Dio:

import 'package:dio/dio.dart';

class ApiService {
  late final Dio _dio;

  ApiService() {
    _dio = Dio(
      BaseOptions(
        baseUrl: 'https://jsonplaceholder.typicode.com',
        connectTimeout: Duration(seconds: 5),
        receiveTimeout: Duration(seconds: 3),
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      ),
    );

    // Add interceptor for logging
    _dio.interceptors.add(
      LogInterceptor(
        requestBody: true,
        responseBody: true,
      ),
    );
  }

  // GET request with error handling
  Future<List<dynamic>> getPosts() async {
    try {
      final response = await _dio.get('/posts');
      return response.data as List<dynamic>;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // POST request
  Future<Map<String, dynamic>> createPost({
    required String title,
    required String body,
  }) async {
    try {
      final response = await _dio.post(
        '/posts',
        data: {
          'title': title,
          'body': body,
          'userId': 1,
        },
      );
      return response.data;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // Centralized error handling
  String _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return 'Connection timeout. Please try again.';

      case DioExceptionType.badResponse:
        // Server responded with error
        final statusCode = error.response?.statusCode;
        if (statusCode == 404) return 'Resource not found';
        if (statusCode == 500) return 'Server error';
        return 'Error: ${error.response?.statusMessage}';

      case DioExceptionType.cancel:
        return 'Request cancelled';

      default:
        return 'Network error. Check your connection.';
    }
  }
}

Using the API Service in a Widget

import 'package:flutter/material.dart';

class PostListScreen extends StatefulWidget {
  @override
  State<PostListScreen> createState() => _PostListScreenState();
}

class _PostListScreenState extends State<PostListScreen> {
  final _apiService = ApiService();
  List<dynamic> _posts = [];
  bool _isLoading = false;
  String? _errorMessage;

  @override
  void initState() {
    super.initState();
    _loadPosts();
  }

  Future<void> _loadPosts() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final posts = await _apiService.getPosts();
      setState(() {
        _posts = posts;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _errorMessage = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Posts')),
      body: _isLoading
          ? Center(child: CircularProgressIndicator())
          : _errorMessage != null
              ? Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(_errorMessage!),
                      SizedBox(height: 16),
                      ElevatedButton(
                        onPressed: _loadPosts,
                        child: Text('Retry'),
                      ),
                    ],
                  ),
                )
              : ListView.builder(
                  itemCount: _posts.length,
                  itemBuilder: (context, index) {
                    final post = _posts[index];
                    return ListTile(
                      title: Text(post['title']),
                      subtitle: Text(post['body']),
                    );
                  },
                ),
    );
  }
}

Advanced: Request Interceptors for Auth

Add authentication tokens automatically to all requests:

class AuthInterceptor extends Interceptor {
  @override
  void onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    // Get token from secure storage
    final token = await getAuthToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    // Handle 401 unauthorized - refresh token or logout
    if (err.response?.statusCode == 401) {
      // Navigate to login or refresh token
    }
    handler.next(err);
  }
}

// Add to your Dio instance
_dio.interceptors.add(AuthInterceptor());

Try It Yourself

Build a complete API integration:

  1. Create a user registration endpoint with POST request
  2. Add loading states and error messages
  3. Implement a retry mechanism for failed requests
  4. Add a timeout indicator showing request progress

Challenge: Create a file upload feature using FormData and show upload progress with a progress bar. Use Dio’s onSendProgress callback.

Tip of the Day

Testing Tip: Create a mock Dio instance for testing by extending Dio and overriding methods, or use the http_mock_adapter package. This lets you test your API layer without making real network calls.

Performance: Reuse a single Dio instance throughout your app (singleton or dependency injection). Creating new instances for every request wastes resources and loses interceptor benefits.