Flutter & Dio: Interceptors for Logging and Error Handling
Building robust mobile applications with Flutter often involves interacting with RESTful APIs. Dio is a powerful HTTP client for Dart and Flutter, providing an elegant way to handle network requests. One of Dio's most compelling features is its support for Interceptors, which allow you to intercept and process requests, responses, and errors globally before they are handled by the calling code. This article explores how to leverage Dio Interceptors for effective logging and comprehensive error handling in your Flutter applications.
Why Use Interceptors?
Interceptors offer several advantages for managing network operations:
- Centralized Logic: Apply common logic (like authentication tokens, logging, or error processing) to all requests in one place, avoiding repetitive code.
- Modifiability: Modify request options before a request is sent or modify a response before it's returned.
- Error Handling: Catch and handle network-related errors consistently across the entire application, improving user experience and simplifying debugging.
- Debugging and Monitoring: Log request and response details, which is crucial for debugging during development and monitoring in production.
Setting Up Dio
First, ensure you have Dio added to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0 # Use the latest stable version
Initialize Dio, which will be used throughout your application:
import 'package:dio/dio.dart';
final Dio dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com', // Your API base URL
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
Logging Interceptor
Logging is vital for debugging network issues and understanding application behavior. Dio provides a built-in LogInterceptor, but you can also create a custom one for more control.
Using Dio's Built-in LogInterceptor
The simplest way to add logging is by using LogInterceptor:
dio.interceptors.add(LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
logPrint: (obj) => print(obj), // Custom logging function
));
This interceptor will print detailed information about requests, responses, and errors to the console.
Creating a Custom Logging Interceptor
For more tailored logging, you can extend Interceptor:
import 'package:dio/dio.dart';
import 'dart:developer' as developer; // For better logging in VS Code/Android Studio
class CustomLogInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
developer.log('REQUEST[${options.method}] => PATH: ${options.path}', name: 'DioLog');
developer.log('HEADERS: ${options.headers}', name: 'DioLog');
developer.log('DATA: ${options.data}', name: 'DioLog');
super.onRequest(options, handler);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
developer.log('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}', name: 'DioLog');
developer.log('DATA: ${response.data}', name: 'DioLog');
super.onResponse(response, handler);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
developer.log('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}', name: 'DioLog');
developer.log('MESSAGE: ${err.message}', name: 'DioLog');
developer.log('RESPONSE DATA: ${err.response?.data}', name: 'DioLog');
super.onError(err, handler);
}
}
Error Handling Interceptor
Consistent error handling is crucial for a smooth user experience. An error handling interceptor can catch DioExceptions, categorize them, and potentially transform them into more application-specific exceptions or display user-friendly messages.
import 'package:dio/dio.dart';
class ErrorHandlingInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
String userMessage = 'An unexpected error occurred.';
int? statusCode = err.response?.statusCode;
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
userMessage = 'Connection timeout. Please check your internet connection.';
break;
case DioExceptionType.badResponse:
// Handle HTTP errors based on status code
if (statusCode != null) {
if (statusCode >= 400 && statusCode < 500) {
// Client-side errors (e.g., 401 Unauthorized, 404 Not Found)
if (statusCode == 401) {
userMessage = 'Unauthorized. Please log in again.';
// Optionally, trigger a logout or token refresh
} else if (statusCode == 404) {
userMessage = 'Resource not found. The requested item does not exist.';
} else if (statusCode == 400) {
userMessage = err.response?.data?['message'] ?? 'Bad request. Please check your input.';
} else {
userMessage = 'Client error: $statusCode. ${err.response?.data?['message'] ?? err.message}';
}
} else if (statusCode >= 500 && statusCode < 600) {
// Server-side errors
userMessage = 'Server error: $statusCode. Please try again later.';
}
} else {
userMessage = 'Bad response with no status code: ${err.message}';
}
break;
case DioExceptionType.cancel:
userMessage = 'Request cancelled.';
break;
case DioExceptionType.badCertificate:
userMessage = 'SSL certificate error. Cannot establish a secure connection.';
break;
case DioExceptionType.connectionError:
userMessage = 'Failed to connect to the server. Check your internet connection.';
break;
case DioExceptionType.unknown:
default:
userMessage = 'Something went wrong: ${err.message}.';
break;
}
// You can re-throw a custom exception here
// For example, throw AppNetworkException(userMessage, originalError: err);
// Or, update the DioException message so that UI can display it
handler.next(err.copyWith(message: userMessage));
}
}
In this interceptor, we categorize errors based on DioExceptionType and HTTP status codes, providing more meaningful messages. You can integrate this with a global error display mechanism (e.g., showing a SnackBar or a dialog).
Integrating Interceptors with Dio
To make your interceptors active, add them to Dio's interceptors list:
final Dio dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
// Add the custom logging interceptor
dio.interceptors.add(CustomLogInterceptor());
// Add the error handling interceptor
dio.interceptors.add(ErrorHandlingInterceptor());
// Interceptors are executed in the order they are added.
// It's generally good practice to have logging first, then error handling.
Example Usage in Your Flutter App
Now, when you make an API call using this Dio instance, the interceptors will automatically process the requests, responses, and errors.
Future fetchData() async {
try {
final response = await dio.get('/data'); // Relative path will use baseUrl
print('Data fetched successfully: ${response.data}');
// Process your data here
} on DioException catch (e) {
// This catch block will receive the error after interceptors have processed it.
// The error message might have been modified by the ErrorHandlingInterceptor.
print('Error in fetchData: ${e.message}');
// Display error message to the user, e.g., using a SnackBar
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.message ?? '')));
} catch (e) {
print('An unexpected non-Dio error occurred: $e');
}
}
Future postData(Map payload) async {
try {
final response = await dio.post('/submit', data: payload);
print('Data posted successfully: ${response.data}');
} on DioException catch (e) {
print('Error in postData: ${e.message}');
// Handle error display
}
}
Conclusion
Dio Interceptors provide a powerful and flexible mechanism for centralizing common network tasks in your Flutter applications. By implementing custom interceptors for logging and error handling, you can significantly improve the maintainability, debuggability, and robustness of your app. This approach ensures a consistent user experience by handling network issues gracefully and providing valuable insights during development and production.