Network Cache Interceptor
Network Cache Interceptor is a custom Dio interceptor for caching network requests. It delivers cached data when offline, supports stale-while-revalidate, optional at-rest encryption, and fine-grained cache lifecycle control.
📦 Installation
Add the following to your pubspec.yaml:
dependencies:
network_cache_interceptor: ^3.0.0
Or install it via flutter pub add:
flutter pub add network_cache_interceptor
🚀 What’s New in Version 3.0.0
Version 3.0.0 is a major release focused on privacy-by-default, cache lifecycle control, and observability.
⚠️ Breaking changes:
- Opt-in storage by default (
storeOnlyOptIn: true) — only requests that opt into caching (extra['cache']set) are written to disk. SetstoreOnlyOptIn: falsefor the old “cache every GET” behavior. - Opt-in offline fallback by default (
offlineFallbackOnlyOptIn: true). - Configuration is applied once — the first
NetworkCacheInterceptor(...)configures the singleton; later calls return it unchanged. UseNetworkCacheInterceptor.instanceto access it.
✅ New features:
'refresh'mode +cachedThenFresh()stream for stale-while-revalidate.- Cache-hit markers (
response.extra['from_cache'],['cached_at']). - Original status code & headers are preserved on cache hits.
CacheMissExceptionforonly_cachemisses.- Lifecycle API:
invalidate(),deleteExpired(),maxEntrieseviction. - Automatic encryption key rotation and
Durationvalidity support.
🚀 Usage
1. Configure Dio
import 'package:dio/dio.dart';
import 'package:network_cache_interceptor/network_cache_interceptor.dart';
void main() {
final dio = Dio();
dio.interceptors.add(
NetworkCacheInterceptor(
noCacheStatusCodes: [401, 403, 304],
noCacheHttpMethods: ['POST', 'PUT'], // Methods that are never cached
cacheValidity: const Duration(minutes: 30),
getCachedDataWhenError: true,
maxEntries: 200, // Optional: cap stored entries (LRU-ish)
// storeOnlyOptIn defaults to true — only opt-in requests are stored.
encryptionKey: 'my_secret_key', // Optional: encrypt cache at rest
cacheWhen: (response) => // Optional: only cache when this is true
response.data is Map && response.data['success'] == true,
),
);
}
Configure the interceptor once at startup. Later
NetworkCacheInterceptor()calls return the same instance and ignore their arguments. Reach it anywhere withNetworkCacheInterceptor.instance.
2. Cache Modes
Caching is opted into per request via options.extra['cache']:
| Value | Behavior |
|---|---|
true |
Serve a valid cached response if present, else hit the network and store |
'only_cache' |
Serve a valid cached response, else fail fast (no network call) |
'refresh' |
Always hit the network and store, but never serve from cache |
absent/false |
No caching for this request |
final response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
options: Options(
extra: {
'cache': true,
'validate_time': const Duration(minutes: 60), // int minutes also accepted
},
),
);
3. Use unique_key for More Precise Caching
unique_key ensures different cache entries for similar requests:
final response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
options: Options(
extra: {'cache': true, 'unique_key': 'user_123'},
),
);
4. Offline-Only Reads with only_cache
Return cached data instantly and skip the network entirely. If no valid cache exists, the request fails fast with a DioException carrying a CacheMissException:
try {
final response = await dio.get(
'/posts',
options: Options(extra: {'cache': 'only_cache'}),
);
print(response.data); // Served from cache, no network call
} on DioException catch (e) {
if (e.error is CacheMissException) {
print('No cached data available');
}
}
5. Stale-While-Revalidate with cachedThenFresh
Render cached data instantly, then update when the network responds:
NetworkCacheInterceptor.instance
.cachedThenFresh(dio, '/orders')
.listen((response) {
final fromCache = response.extra['from_cache'] == true;
render(response.data, stale: fromCache);
});
The stream emits the cached response first (if any), then the fresh network response. Prefer this over hand-rolling the two-legged pattern in every bloc/provider.
6. Detect Cache Hits
Served-from-cache responses are tagged, so the UI can show an “offline data” banner:
if (response.extra['from_cache'] == true) {
final cachedAt = DateTime.parse(response.extra['cached_at']);
showBanner('Showing data from ${cachedAt.toLocal()}');
}
Cache hits also restore the original statusCode and response headers.
7. Encrypt the Cache at Rest
Pass an encryptionKey (1-32 characters) to encrypt cached data and cache keys:
dio.interceptors.add(
NetworkCacheInterceptor(encryptionKey: 'my_secret_key'),
);
Response bodies use AES-GCM (random IV per entry); cache keys use deterministic AES-CBC so lookups stay consistent. If the key changes, entries written with the old key are detected and purged automatically.
8. Invalidate, Expire, and Clear
final cache = NetworkCacheInterceptor.instance;
// Remove every cached variant of one endpoint (e.g. after a POST /orders).
await cache.invalidate('https://api.example.com/orders');
// Prune entries older than a duration.
await cache.deleteExpired(const Duration(days: 7));
// Remove everything.
await cache.clearDatabase();
The maxEntries constructor option caps the store and evicts the oldest entries automatically.
⚙️ Configuration
| Parameter | Description | Default |
|---|---|---|
noCacheStatusCodes |
HTTP status codes that should not be cached | [401, 403, 304] |
noCacheHttpMethods |
HTTP methods that should not be cached | ['POST'] |
cacheValidityMinutes |
Cache validity in minutes (ignored if cacheValidity is set) |
30 |
cacheValidity |
Cache validity as a Duration |
null |
getCachedDataWhenError |
Return cached data on connectivity errors | true |
uniqueWithHeader |
Use request headers for unique cache keys | false |
storeOnlyOptIn |
Only store responses whose request opted in | true |
offlineFallbackOnlyOptIn |
Only use the offline fallback for opt-in requests | true |
maxEntries |
Cap on stored entries (oldest evicted) | null (unlimited) |
cacheWhen |
Predicate to restrict which responses get cached | null (cache all) |
encryptionKey |
AES key (1-32 chars) to encrypt the cache at rest | null (no encryption) |
Per-request extra keys: cache, validate_time (int minutes or Duration), unique_key, cache_updated_date.
🍳 Recipes
Cache-then-network (stale-while-revalidate):
NetworkCacheInterceptor.instance
.cachedThenFresh(dio, '/dashboard')
.listen((res) => emit(DashboardState(res.data, stale: res.extra['from_cache'] == true)));
Privacy gating — cache only non-sensitive, successful bodies:
NetworkCacheInterceptor(
storeOnlyOptIn: true, // never store anything not explicitly opted in
cacheWhen: (r) => r.data is Map && r.data['success'] == true,
);
🔧 Technical Details
- Offline Mode: Cached responses are returned on timeouts, no connection, or socket errors.
- Opt-in by Default: Only requests that set
extra['cache']are stored (configurable). - Encryption at Rest: Optional AES for data (AES-GCM) and keys (AES-CBC), with automatic key-rotation cleanup.
- Observability:
from_cache/cached_atmarkers and preserved status codes & headers. - Header Filtering: Ignores
Authorization,User-Agent, andcontent-lengthin cache keys. - Robust Storage: Local SQLite with size (
maxEntries) and age (deleteExpired) controls.
🛡️ License
This project is licensed under the MIT License.
💬 Additional Information
For more information or to contribute, visit our GitHub page.
📦 Explore More
If you enjoy using this package, check out:
- Telegram Bot Crashlytics – Send app crashes and errors to a Telegram chat for easier monitoring.
Thanks for using Network Cache Interceptor! 🚀