A powerful, lightweight API response caching solution for Flutter with TTL, offline-first support, stale-while-revalidate, and Dio interceptor.
- 🗂️ 5 caching strategies — cacheFirst, networkFirst, staleWhileRevalidate, networkOnly, cacheOnly
- 🔌 Dio interceptor with automatic caching — drop-in, zero-boilerplate integration
- ⚡ Dual-layer caching — in-memory LRU cache + persistent disk storage via Hive
- ⏱️ Configurable TTL per endpoint — fine-grained control over cache freshness
- 📡 Offline-first support — automatic cache fallback on network errors
- 🔄 Stale-while-revalidate with background refresh callback
- 🔑 Smart cache key generation using MD5 hashing
- 🧹 LRU eviction by count, size, and TTL expiry
- 🎯 Per-request and URL-based policy configuration
- 🚀 Force refresh support — bypass cache on demand
- 📊 Cache statistics and manual invalidation
Add flutter_api_cache to your pubspec.yaml:
dependencies:
flutter_api_cache: ^0.0.1Then run:
flutter pub getimport 'package:dio/dio.dart';
import 'package:flutter_api_cache/flutter_api_cache.dart';
Future<void> main() async {
// 1. Create and initialize the cache manager
final cacheManager = ApiCacheManager(
config: const CacheConfig(
defaultTtlSeconds: 300,
maxEntries: 500,
enableLogging: true,
),
);
await cacheManager.init();
// 2. Create Dio and attach the cache interceptor
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(CacheInterceptor(cacheManager: cacheManager));
// 3. Make requests — caching is handled automatically
final response = await dio.get('/users');
print('From cache: ${response.extra['fromCache'] ?? false}');
}| Strategy | Behavior | Best For |
|---|---|---|
cacheFirst |
Returns cache if available and fresh, otherwise fetches from network. | Mostly static data |
networkFirst |
Always tries network first, falls back to cache on failure. | Frequently updated data |
staleWhileRevalidate |
Returns cache immediately (even if stale), refreshes in background. | Feeds and lists |
networkOnly |
Always fetches from network, never uses or stores cache. | Auth, payments |
cacheOnly |
Only returns cached data, never makes a network call. | Offline mode |
Override the default caching policy for a specific request using cachePolicyKey in request extras:
final response = await dio.get(
'/users/1',
options: Options(
extra: {
cachePolicyKey: const CachePolicy(
strategy: CacheStrategy.networkFirst,
ttlSeconds: 600,
),
},
),
);Configure different policies for different URL patterns via the interceptor's policyMap:
dio.interceptors.add(
CacheInterceptor(
cacheManager: cacheManager,
policyMap: {
'/users': const CachePolicy(
strategy: CacheStrategy.cacheFirst,
ttlSeconds: 300,
),
'/config': const CachePolicy(
strategy: CacheStrategy.cacheFirst,
ttlSeconds: 3600, // Config rarely changes
),
'/feed': const CachePolicy(
strategy: CacheStrategy.staleWhileRevalidate,
ttlSeconds: 60,
),
'/auth': CachePolicy.none, // Never cache auth endpoints
},
),
);Bypass the cache and fetch fresh data from the network:
final response = await dio.get(
'/users',
options: Options(
extra: {
cachePolicyKey: const CachePolicy(forceRefresh: true),
},
),
);React to background refreshes when using the stale-while-revalidate strategy:
cacheManager.onBackgroundUpdate = (key, entry) {
print('Background refresh completed for: $key');
// Update your UI here when fresh data arrives
};// Invalidate a specific endpoint
await cacheManager.invalidate('https://api.example.com/users/1');
// Invalidate all entries matching a predicate
await cacheManager.invalidateWhere((key) => key.contains('users'));
// Clear the entire cache
await cacheManager.clearAll();
// Get cache statistics
final stats = await cacheManager.getStats();
print(stats); // CacheStats(entries: 42, size: 0.15 MB, memory: 12)
// Dispose when done
await cacheManager.dispose();Implement the CacheStorage abstract class to provide your own storage backend (e.g. SQLite, shared preferences, or a remote cache). Pass your custom implementation as the diskStorage parameter when creating ApiCacheManager.
final cacheManager = ApiCacheManager(
diskStorage: MyCustomStorage(),
);final cacheManager = ApiCacheManager(
config: const CacheConfig(
defaultTtlSeconds: 300, // Default TTL: 5 minutes
maxEntries: 500, // Max cached entries before LRU eviction
maxSizeBytes: 0, // Max cache size in bytes (0 = unlimited)
enableLogging: false, // Print cache operations to console
boxName: 'flutter_api_cache', // Hive box name for disk storage
useMemoryCache: true, // Enable in-memory LRU cache layer
maxMemoryEntries: 100, // Max entries in the memory cache
),
);- Request arrives → the Dio interceptor resolves the caching policy and checks the cache.
- Cache hit → the cached response is returned immediately, skipping the network call.
- Cache miss → the request is forwarded to the network; the response is cached on success.
- Network error → the interceptor falls back to stale cached data when available.
- Background → the eviction manager periodically cleans expired entries and enforces count/size limits.
Contributions are welcome! To get started:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes and write tests
- Ensure all checks pass before submitting:
dart analyze dart format . flutter test
- Submit a pull request
This project is licensed under the MIT License — see the LICENSE file for details.