Making HTTP Requests in Flutter with Dio: A Step-by-Step Tutorial

Dio Flutter is a widely used HTTP client library that makes HTTP requests much easier with useful features like interceptors, request cancellation, error handling, etc.

Making HTTP Requests in Flutter with Dio Package

HTTP requests are crucial in mobile application development as they connect the app with remote servers. They handle essential tasks like retrieving data, submitting forms, and syncing information across devices. Without efficient HTTP request management, apps can become slow or unresponsive, which can harm the user experience.

Dio is a widely used HTTP client library for Flutter that makes HTTP requests much easier. It's a great choice for Flutter developers because its API has many useful features, such as interceptors, request cancellation, error handling, and file uploads/downloads.

>> Read more:

Setting Up Dio in Flutter

Installing Dio

To get started with Dio, you need to add it to your Flutter project as a dependency. Simply update your pubspec.yaml file and run the Flutter CLI to fetch the package.

Always use the latest Dio version to stay updated with new features and security patches.

Creating a Dio Instance

Before making HTTP requests, create a Dio instance. Configuring it with BaseOptions helps you manage settings like the base URL, headers, and timeouts in one place.

  • Base URL: Automatically appended to endpoints in requests, saving repetitive typing and reducing errors.
  • Timeouts: Ensure your app doesn't hang indefinitely if a server is unresponsive.
  • Headers: Set default headers like Authorization tokens for secure endpoints.

Example configuration:

typescript
Dio dio = Dio(BaseOptions(
  baseUrl: "https://api.example.com",
  connectTimeout: 5000,  // Timeout in milliseconds
  receiveTimeout: 3000,  // Timeout in milliseconds
  headers: {
    "Authorization": "Bearer YOUR_TOKEN",  // Add default auth token
  },
));

Making Basic HTTP Requests (GET/POST/PUT/DELETE)

GET Request

The GET method retrieves data from the server. This is commonly used to fetch resources like user profiles or product lists.

typescript
Response response = await dio.get('/endpoint');
print(response.data);

POST Request

POST sends data to the server to create or update resources. Use this for submitting forms or uploading data.

typescript
Response response = await dio.post('/endpoint', data: {"key": "value"});

PUT Request

The PUT method updates existing resources. It often replaces the entire resource with new data.

typescript
Response response = await dio.put('/endpoint', data: {"key": "updatedValue"});

DELETE Request

DELETE removes resources from the server. This is used to delete records or clear user data.

typescript
Response response = await dio.delete('/endpoint');

Each method supports advanced configurations like query parameters, headers, and custom error handling.

Using Dio Interceptors for Custom Request and Response Handling

Interceptors act as middleware for HTTP requests and responses. They allow you to:

  • Modify requests before they are sent.
  • Log request/response data for debugging.
  • Handle errors globally without duplicating code.

Code Examples:

  • Adding Authentication Tokens: Ensures every request automatically includes the necessary credentials.
typescript
dio.interceptors.add(InterceptorsWrapper(
  onRequest: (options, handler) {
    options.headers['Authorization'] = 'Bearer YOUR_TOKEN';
    return handler.next(options);
  },
));
  • Logging Requests and Responses: Useful during development for debugging.
typescript
dio.interceptors.add(LogInterceptor(
  requestBody: true,
  responseBody: true,
));
  • Global Error Handling: Standardizes how errors are processed across the app.
typescript
dio.interceptors.add(InterceptorsWrapper(
  onError: (DioError error, handler) {
    print("Error: ${error.message}");
    return handler.next(error);
  },
));

Interceptors are powerful tools for customizing Dio's behavior to suit your app's needs.

Cancellation Tokens

Cancellation tokens are used to stop ongoing HTTP requests, saving resources and improving user experience in scenarios like search-as-you-type or navigating away from pages.

How It Works?

  • Create a CancelToken:
typescript
CancelToken cancelToken = CancelToken();
  • Attach it to your request:
typescript
dio.get('/endpoint', cancelToken: cancelToken);
  • Cancel the request:
typescript
cancelToken.cancel("Request cancelled");

This is particularly helpful when dealing with long-running requests that may no longer be relevant.

Error Handling and Retry Strategies

Error handling is a crucial aspect of making HTTP requests. In real-world scenarios, requests can fail for various reasons, including network issues, server errors, or incorrect inputs. By implementing robust error handling and retry strategies, you can improve the resilience and reliability of your application.

Common Error Scenarios

Network Errors:

  • Connection Timeout: The app couldn't establish a connection to the server within the allotted time.
  • Receive Timeout: The server didn't respond with data within the expected time.
  • No Internet Connection: The device is offline or the DNS lookup fails.

HTTP Errors:

  • Client Errors (4xx):
    • 400 (Bad Request): The request is malformed or contains invalid data.
    • 401 (Unauthorized): Authentication is required or the token is invalid.
    • 404 (Not Found): The requested resource doesn't exist.
  • Server Errors (5xx):
    • 500 (Internal Server Error): A generic error on the server side.
    • 503 (Service Unavailable): The server is temporarily overloaded or under maintenance.

Other Scenarios:

  • File upload/download interruptions.
  • DNS resolution issues.
  • Malformed JSON responses from the server.

Basic Error Handling

Dio uses exceptions (DioError) to report errors. Wrapping your HTTP calls in a try-catch block allows you to handle errors gracefully. Example:

typescript
try {
  Response response = await dio.get('/endpoint');
  print("Response data: ${response.data}");
} catch (e) {
  if (e is DioError) {
    print("DioError: ${e.message}");
    handleError(e);
  } else {
    print("Unexpected error: $e");
  }
}

Custom Error Handling

Creating custom error handlers helps in standardizing how your app responds to different error types, improving user experience. For example:

typescript
void handleError(DioError error) {
  switch (error.type) {
    case DioErrorType.connectTimeout:
      print("Connection Timeout. Please try again later.");
      break;
    case DioErrorType.receiveTimeout:
      print("Receive Timeout. Check your network connection.");
      break;
    case DioErrorType.response:
      handleHttpResponseError(error.response!);
      break;
    case DioErrorType.cancel:
      print("Request was cancelled.");
      break;
    default:
      print("Unexpected error: ${error.message}");
  }
}

void handleHttpResponseError(Response response) {
  if (response.statusCode == 401) {
    print("Unauthorized. Please login again.");
  } else if (response.statusCode == 404) {
    print("Resource not found.");
  } else if (response.statusCode! >= 500) {
    print("Server error. Please try again later.");
  } else {
    print("Unexpected HTTP error: ${response.statusCode}");
  }
}

Retry Strategies

Retries are important for transient errors, such as temporary network failures or server unavailability. Implementing retry mechanisms ensures your app doesn't fail prematurely for recoverable errors.

Best Practices for Retries:

  • Retry for Transient Errors Only: Avoid retrying for permanent errors like 401 (Unauthorized) or 404 (Not Found).
  • Exponential Backoff: Add increasing delays between retry attempts to reduce server load and avoid overloading your app. For example: Wait 1 second before the first retry, 2 seconds before the second, and so on.
  • Set Retry Limits: Limit the number of retries to prevent infinite loops. For example, retry a maximum of 3 times.
  • Dynamic Retry Delays: Use response headers like Retry-After (if available) to respect the server's preferred retry interval.

Example: Manual Retry Logic

typescript
Future<Response?> performRequestWithRetry(String endpoint, int maxRetries) async {
  int retryCount = 0;
  while (retryCount < maxRetries) {
    try {
      return await dio.get(endpoint);
    } catch (e) {
      if (e is DioError && retryCount < maxRetries - 1) {
        print("Retry attempt: ${retryCount + 1}");
        await Future.delayed(Duration(seconds: retryCount + 1)); // Exponential backoff
        retryCount++;
      } else {
        throw e; // Exit if max retries reached or non-transient error occurs
      }
    }
  }
  return null;
}

Using Plugins for Retry

Instead of writing manual retry logic, you can use plugins like dio_retry, which provides a prebuilt retry interceptor. Here is an example of retry Interceptor:

typescript
import 'package:dio_retry/dio_retry.dart';

Dio dio = Dio();

// Add Retry Interceptor
dio.interceptors.add(
  RetryInterceptor(
    dio: dio,
    retries: 3,
    retryDelays: [
      Duration(seconds: 1),
      Duration(seconds: 2),
      Duration(seconds: 4), // Exponential backoff
    ],
    retryEvaluator: (error) =>
        error.type != DioErrorType.cancel && error.type != DioErrorType.response,
  ),
);

Displaying User-Friendly Messages

Errors should be presented to users in a way they can understand. For example:

  • "The server is currently unavailable. Please try again later."
  • "Your request took too long to process. Check your internet connection and try again."

For developers, ensure error messages in logs are detailed to facilitate debugging.

File Uploads and Downloads

File Upload

Uploading files involves creating a FormData object and sending it with the POST method.

typescript
FormData formData = FormData.fromMap({
  "file": await MultipartFile.fromFile("path/to/file"),
});

Response response = await dio.post('/upload', data: formData);

File Download

Downloading files is straightforward with Dio's download method, which supports progress tracking:

typescript
Response response = await dio.download(
  'https://example.com/file',
  'path/to/save/file',
  onReceiveProgress: (received, total) {
    print("Progress: ${received / total * 100}%");
  },
);

Best Practices for File Handling

  • Compression:

    • Compress large files before uploading to reduce upload time and server storage requirements. Use libraries for image compression (e.g., image_picker for Flutter).
    • Similarly, download compressed files and decompress them on the client-side.
  • Chunking:
    • Break large files into smaller chunks before uploading or downloading to avoid memory overload and improve performance.
    • Dio supports chunked uploads and downloads with proper configuration.
    • Chunked Upload Example: Upload chunks sequentially with offset tracking, ensuring data integrity if interruptions occur.
  • Progress Tracking:

    • Use onReceiveProgress (downloads) and onSendProgress (uploads) to display progress to users, especially for large files.
  • Error Handling: Implement robust error handling for scenarios like:

    • Network interruptions: Retry logic with exponential backoff.
    • File corruption: Validate checksums or use hashing (e.g., MD5).
    • Server errors: Display meaningful messages for HTTP errors like 413 (Payload Too Large).

     

  • Secure File Handling:

    • Use HTTPS to encrypt file transfers.
    • Secure file storage on the client side (e.g., using sandboxed directories).
    • Validate uploaded files on the server to prevent malicious content.
  • Memory Optimization:

    • Avoid loading large files entirely into memory. Stream files directly to and from storage to minimize memory usage.
  • File Type and Size Validation:

    • Validate file types and sizes before uploads to prevent overloading the server and ensure compliance with server-side constraints.
  • Retry Mechanisms:

    • For unreliable networks, implement retry mechanisms for interrupted uploads/downloads, possibly resuming from where the transfer stopped.

Testing Dio HTTP Requests

Unit Testing

Write unit tests to verify request behavior:

typescript
test('Dio GET Test', () async {
  final dio = Dio();
  final response = await dio.get('https://jsonplaceholder.typicode.com/posts');
  expect(response.statusCode, 200);
});

Mocking API Requests

Mock requests to simulate server responses without making real network calls:

typescript
class MockAdapter extends HttpClientAdapter {
  @override
  Future<ResponseBody> fetch(RequestOptions options, Stream<List<int>> requestStream, Future cancelFuture) async {
    return ResponseBody.fromString('{"key":"value"}', 200, headers: {});
  }
}

>> You may consider:

Conclusion

By leveraging Dio, developers gain a robust toolset for managing HTTP requests in Flutter. From interceptors to error handling and testing, Dio enhances productivity and application reliability. Explore its advanced features to take full advantage of this powerful library in your next project.

>>> Follow and Contact Relia Software for more information!

  • coding