network_cache_interceptor 3.0.0
network_cache_interceptor: ^3.0.0 copied to clipboard
NetworkCacheInterceptor is a custom interceptor designed to optimize network requests by integrating caching functionality into your application using the Dio library.
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! ๐