Flutter Dio Interceptor for Caching API Responses
Source: Dev.to
How the Caching Interceptor Works
| Feature | Description |
|---|---|
| Persist GET responses | Only GET method responses are cached, as these typically represent data‑retrieval operations that are safe to cache. |
| Network‑first strategy | When network connectivity is available, the interceptor fetches fresh data from the API. |
| Fallback to cache | If a network error occurs, the interceptor automatically falls back to previously cached responses. |
| Automatic cache updates | Successful responses automatically update the cache for future offline access. |
1️⃣ Storage Abstraction Layer
Creating a storage abstraction gives you the flexibility to swap out the underlying storage mechanism (e.g., SharedPreferences, Hive, Secure Storage) without touching the interceptor logic.
/// Simple key‑value storage interface for caching
abstract class CacheStorage {
Future get(String key);
Future set(String key, String value);
Future remove(String key);
}
/// Implementation using SharedPreferences (via AppPreference)
class CacheStorageImpl implements CacheStorage {
CacheStorageImpl(this._pref);
final AppPreference _pref;
@override
Future get(String key) async => _pref.getString(key);
@override
Future set(String key, String value) async =>
await _pref.setString(key, value);
@override
Future remove(String key) async => await _pref.remove(key);
}
2️⃣ Caching Interceptor Implementation
import 'dart:convert';
import 'package:dio/dio.dart';
class CacheInterceptor implements Interceptor {
final CacheStorage storage;
CacheInterceptor(this.storage);
// -------------------------------------------------
// 1️⃣ onRequest – let the request pass through unchanged
// -------------------------------------------------
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) {
handler.next(options);
}
// -------------------------------------------------
// 2️⃣ onResponse – cache successful GET responses
// -------------------------------------------------
@override
void onResponse(
Response response, ResponseInterceptorHandler handler) {
if (response.requestOptions.method.toUpperCase() == 'GET') {
_saveResponseToCache(response);
}
handler.next(response);
}
// -------------------------------------------------
// 3️⃣ onError – try to serve cached data on network errors
// -------------------------------------------------
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Attempt to retrieve cached data for GET requests
if (err.requestOptions.method.toUpperCase() == 'GET') {
final cached = await _getCachedResponse(err.requestOptions);
if (cached != null) {
// Return cached response instead of the error
return handler.resolve(cached);
}
}
// Show a “no internet” modal for network‑related errors
if (_isNetworkError(err)) {
await NoInternetModalWidget.show();
}
// Propagate the original error if no cache is available
handler.next(err);
}
// -------------------------------------------------
// Helper methods
// -------------------------------------------------
bool _isNetworkError(DioException err) {
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError;
}
Future _getCachedResponse(RequestOptions options) async {
try {
final cacheKey = '${options.uri}';
final cacheEntry = await storage.get(cacheKey);
if (cacheEntry == null) return null;
final Map decodedCache = jsonDecode(cacheEntry);
return Response(
requestOptions: options,
data: decodedCache,
statusCode: 200,
);
} catch (_) {
// Corrupted cache – ignore and let the error propagate
return null;
}
}
Future _saveResponseToCache(Response response) async {
final cacheKey = '${response.realUri}';
final cacheEntry = jsonEncode(response.data);
await storage.set(cacheKey, cacheEntry);
}
}
Lifecycle Method Summary
| Method | What It Does |
|---|---|
| onRequest | Passes the request through unchanged. |
| onResponse | If the request was a GET, stores the response in the cache using the full URI as the key. |
| onError | For failed GET requests, attempts to retrieve a cached response. If found, resolves the error with the cached data; otherwise, shows a “no internet” modal (for network errors) and propagates the error. |
3️⃣ Using the Interceptor with Dio
import 'package:dio/dio.dart';
class ApiClient {
late final Dio dio;
ApiClient(AppPreference appPreference) {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
),
);
// 1️⃣ Create the cache storage implementation
final cacheStorage = CacheStorageImpl(appPreference);
// 2️⃣ Add the cache interceptor
dio.interceptors.add(CacheInterceptor(cacheStorage));
// 3️⃣ (Optional) Add other interceptors such as logging, auth, etc.
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
),
);
}
}
Now every GET request made through ApiClient will:
- Try the network first – fetch fresh data when possible.
- Cache the response – store successful results for offline use.
- Fall back to cache – serve the cached data automatically when the network fails.
🎉 Wrap‑Up
- The CacheInterceptor provides a clean, reusable way to add offline support to any Dio‑based Flutter app.
- By abstracting storage behind
CacheStorage, you can switch fromSharedPreferencesto Hive, SQLite, or secure storage with a single line of code. - The network‑first strategy ensures users always see the most up‑to‑date information when they’re online, while still delivering a functional experience when they’re not.
Transparent Caching with Dio Interceptor
class ProductRepository {
final ApiClient apiClient;
ProductRepository(this.apiClient);
Future> getProducts() async {
try {
final response = await apiClient.dio.get('/products');
return (response.data as List)
.map((json) => Product.fromJson(json))
.toList();
} catch (e) {
// Handle error or rethrow
rethrow;
}
}
}
When network is available, this fetches fresh data and caches it.
When offline, it returns the cached response without throwing an error.
🔄 Transparent Caching
No changes required to existing API calls – caching works automatically.
📴 Offline Resilience
Automatic fallback to cached data during network failures.
🎯 Clean Architecture
Separation of concerns through storage abstraction.
🔌 Easy Integration
Simple integration with dependency‑injection patterns.
Possible Extensions
Add timestamps to determine cache freshness
Future _saveResponseToCache(Response response) async {
final cacheKey = '${response.realUri}';
final cacheData = {
'data': response.data,
'timestamp': DateTime.now().millisecondsSinceEpoch,
};
final cacheEntry = jsonEncode(cacheData);
await storage.set(cacheKey, cacheEntry);
}
Implement LRU (Least Recently Used) cache eviction
class CacheManager {
final int maxCacheSize;
final CacheStorage storage;
Future evictOldestCache() async {
// Implementation for removing oldest cached entries
}
}
Configure different caching behaviors per endpoint
class CacheConfig {
final Duration? ttl;
final bool enabled;
CacheConfig({this.ttl, this.enabled = true});
}
// Usage
final cacheConfig = {
'/products': CacheConfig(ttl: Duration(hours: 1)),
'/user/profile': CacheConfig(enabled: false),
};
Implementing a caching interceptor for Dio provides significant benefits for Flutter applications. This approach offers a robust offline‑first experience where users can continue accessing previously loaded content even without internet connectivity.
The storage‑abstraction pattern ensures flexibility and maintainability, while the interceptor seamlessly handles the complexity of cache management without cluttering your business logic. The network‑first strategy guarantees fresh data when possible while maintaining functionality during connectivity issues.
Bottom line: This interceptor gives you a solid foundation for building resilient Flutter apps that gracefully handle network instability while delivering an excellent user experience. Start with the basic implementation above and extend it to meet your specific requirements.
Found this article helpful? Share it with your fellow Flutter developers!
Happy Coding!