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:
- Request/response interceptors (for auth tokens, logging)
- Global configuration and base URLs
- Request cancellation
- File upload/download with progress tracking
- Better error handling with typed exceptions
- FormData support for multipart requests
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:
- Create a user registration endpoint with POST request
- Add loading states and error messages
- Implement a retry mechanism for failed requests
- 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.