Advanced Usage of the Dio Package (Interceptors, Cancellation, File Download) in Flutter

The dio package is a powerful HTTP client for Flutter that provides a range of features for making network requests. While basic usage covers simple GET and POST requests, the true potential of dio lies in its advanced features such as interceptors, request cancellation, and file downloading. Mastering these features allows for more robust, efficient, and user-friendly applications.

Introduction to dio

dio supports various HTTP methods, request and response transformations, file uploads and downloads, timeouts, interceptors, and global configuration. Its flexible and extensible design makes it an ideal choice for complex network interactions in Flutter applications.

Why Use Advanced dio Features?

  • Interceptors: Allow you to intercept and modify HTTP requests and responses globally, useful for logging, authentication, or retrying failed requests.
  • Cancellation: Provides the ability to cancel ongoing requests, which is crucial for optimizing network usage and improving user experience, especially in scenarios where users navigate away from a screen.
  • File Download: Enables efficient file downloads with progress tracking, crucial for applications that need to handle large file transfers.

Prerequisites

Before diving into advanced usage, make sure you have the dio package added to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  dio: ^5.3.0  # Use the latest version

Then, run flutter pub get to install the package.

1. Using Interceptors

Interceptors in dio allow you to intercept requests before they are sent and responses before they are delivered to your application. This can be used for global configurations, such as adding authentication headers, logging requests, or handling errors.

Implementing an Interceptor

import 'package:dio/dio.dart';

class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    print('REQUEST[${options.method}] => PATH: ${options.path}');
    return super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
    return super.onResponse(response, handler);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
    return super.onError(err, handler);
  }
}

In this example, the LoggingInterceptor logs the request method and path before sending the request, the response status code and path upon receiving a response, and any errors that occur during the request.

Adding Interceptors to dio Instance

To use the interceptor, add it to your dio instance:

import 'package:dio/dio.dart';

void main() async {
  final dio = Dio();
  dio.interceptors.add(LoggingInterceptor());

  try {
    final response = await dio.get('https://jsonplaceholder.typicode.com/todos/1');
    print('Response data: ${response.data}');
  } catch (e) {
    print('Error: $e');
  }
}

2. Cancelling Requests

Being able to cancel HTTP requests is vital for improving the user experience. For instance, if a user navigates away from a screen before a request completes, you should cancel the request to free up resources.

Using a CancelToken

dio provides a CancelToken to cancel requests:

import 'package:dio/dio.dart';

void main() async {
  final dio = Dio();
  final cancelToken = CancelToken();

  try {
    Future.delayed(Duration(seconds: 5), () {
      cancelToken.cancel('Operation cancelled due to timeout.');
    });

    final response = await dio.get(
      'https://jsonplaceholder.typicode.com/todos/1',
      cancelToken: cancelToken,
    );
    print('Response data: ${response.data}');
  } on DioException catch (e) {
    if (CancelToken.isCancel(e)) {
      print('Request cancelled: ${e.message}');
    } else {
      print('Error: $e');
    }
  }
}

In this example, the request is cancelled after 5 seconds if it hasn’t completed. The CancelToken.isCancel(e) checks if the error was due to cancellation.

Real-World Scenario: Search Feature

Consider a search feature where you want to cancel previous requests when the user types a new query. This prevents race conditions and outdated results from appearing.

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class SearchScreen extends StatefulWidget {
  @override
  _SearchScreenState createState() => _SearchScreenState();
}

class _SearchScreenState extends State {
  final dio = Dio();
  CancelToken? _cancelToken;
  String _searchResults = '';

  Future _search(String query) async {
    if (_cancelToken != null) {
      _cancelToken!.cancel('New search started');
    }
    _cancelToken = CancelToken();

    try {
      final response = await dio.get(
        'https://api.example.com/search?q=$query',  // Replace with your API endpoint
        cancelToken: _cancelToken,
      );
      setState(() {
        _searchResults = response.data.toString();  // Adjust based on your API response
      });
    } on DioException catch (e) {
      if (CancelToken.isCancel(e)) {
        print('Search cancelled: ${e.message}');
      } else {
        print('Search error: $e');
      }
    } finally {
      _cancelToken = null;  // Reset cancel token after completion/cancellation
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Search')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(
              onChanged: (text) {
                _search(text);
              },
              decoration: InputDecoration(labelText: 'Search Query'),
            ),
            SizedBox(height: 20),
            Text('Results: $_searchResults'),
          ],
        ),
      ),
    );
  }
}

3. File Download with Progress Tracking

Downloading files efficiently and providing users with progress updates is essential for applications that handle file transfers. dio makes it easy to download files and track the progress.

Downloading a File

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

void main() async {
  final dio = Dio();
  final url = 'https://www.example.com/large_file.zip'; // Replace with your file URL
  final savePath = '/path/to/save/file.zip';          // Replace with your desired path

  try {
    await dio.download(
      url,
      savePath,
      onReceiveProgress: (received, total) {
        if (total != -1) {
          print('${(received / total * 100).toStringAsFixed(0)}%');
        }
      },
    );
    print('File downloaded successfully!');
  } catch (e) {
    print('Error downloading file: $e');
  }
}

The dio.download method takes the URL of the file and the path where you want to save it. The onReceiveProgress callback provides the number of bytes received and the total file size, allowing you to calculate and display the download progress.

Displaying Download Progress in UI

Here’s how to display the download progress in a Flutter UI:

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';  // Add this dependency

class DownloadScreen extends StatefulWidget {
  @override
  _DownloadScreenState createState() => _DownloadScreenState();
}

class _DownloadScreenState extends State {
  final dio = Dio();
  double _downloadProgress = 0.0;
  bool _isDownloading = false;
  String _filePath = '';

  Future _downloadFile() async {
    setState(() {
      _isDownloading = true;
      _downloadProgress = 0.0;
    });

    try {
      final url = 'https://www.easygifanimator.net/images/samples/video-to-gif-sample.gif';  // Replace with a larger file
      final dir = await getApplicationDocumentsDirectory();
      _filePath = '${dir.path}/my_downloaded_file.gif'; //  File name example

      await dio.download(
        url,
        _filePath,
        onReceiveProgress: (received, total) {
          if (total != -1) {
            setState(() {
              _downloadProgress = received / total;
            });
          }
        },
      );
      print('File downloaded successfully!');
    } catch (e) {
      print('Error downloading file: $e');
    } finally {
      setState(() {
        _isDownloading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('File Download')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_isDownloading)
              Column(
                children: [
                  Text('Downloading... ${(_downloadProgress * 100).toStringAsFixed(0)}%'),
                  SizedBox(height: 10),
                  LinearProgressIndicator(value: _downloadProgress),
                ],
              )
            else
              ElevatedButton(
                onPressed: _downloadFile,
                child: Text('Download File'),
              ),
             if(_filePath.isNotEmpty)
                Padding(
                  padding: const EdgeInsets.only(top: 20.0),
                  child: Text("File saved to: $_filePath"),
                )
          ],
        ),
      ),
    );
  }
}

Key points:

  • Use getApplicationDocumentsDirectory() to get a suitable location for the downloaded file. Make sure to add path_provider to your pubspec.yaml.
  • The UI updates based on _downloadProgress, showing a LinearProgressIndicator during the download.
  • Error handling ensures the UI is updated correctly if the download fails.

Conclusion

The dio package in Flutter provides a robust set of features for handling network requests efficiently. By leveraging interceptors, cancellation tokens, and file download capabilities, you can build more responsive, reliable, and user-friendly applications. These advanced techniques allow you to manage network operations more effectively, providing a better overall experience for your users.