dio_mcache 0.1.0
dio_mcache: ^0.1.0 copied to clipboard
Hyper-configurable HTTP cache interceptor for Dio. Built on mcache_dart. Per-request cache policies, tag-based invalidation, request deduplication, conditional requests, custom serialization, stale-wh [...]
dio_mcache #
Hyper-configurable HTTP cache interceptor for Dio. Built on
mcache_dart.
Every caching decision — TTL, key strategy, serialization, dedup, invalidation, conditional fetch — is controllable globally and per-request.
Install #
dart pub add dio_mcache
dependencies:
dio_mcache: ^0.1.0
Quick start #
import 'package:dio_mcache/dio_mcache.dart';
final dio = Dio()..interceptors.add(DioCacheInterceptor(
options: DioCacheOptions(expiration: const Duration(minutes: 5)),
));
final r1 = await dio.get('/products'); // network
final r2 = await dio.get('/products'); // from cache, instant
print(r2.extra['fromCache']); // true
Architecture #
Request
│
▼
DioCacheInterceptor
├── onRequest → check cache (key → MemoryCache → hit? → serve)
│ ├── dedup check (pending identical request?)
│ ├── conditional headers (ETag / If-None-Match)
│ └── stale-while-revalidate (serve stale + bg refresh)
│
├── onResponse → store cache (policy resolution → serialize → set)
│ ├── auto-invalidate related GETs on mutation
│ ├── tag registration
│ └── max-size / status-code filter
│
└── onError → cleanup dedup entries
Cache policies — organize by data type #
Define named policies for different API patterns, switch per-request:
final dio = Dio()..interceptors.add(DioCacheInterceptor(
options: DioCacheOptions(
expiration: const Duration(minutes: 5),
policies: {
'user-data': const CachePolicy(
expiration: Duration(minutes: 1),
tags: ['user'],
),
'static-assets': const CachePolicy(
expiration: Duration(hours: 24),
priority: CacheItemPriority.low,
),
},
),
));
// Use a policy by name
await dio.get('/profile', options: Options(
extra: {'cachePolicy': 'user-data'},
));
Per-request granular control #
Every global setting can be overridden per request via Options.extra:
| Extra key | Type | Description |
|---|---|---|
cachePolicy |
String |
Named policy from global config |
perRequestCacheOptions |
CachePolicy |
Full override of every cache option |
cacheControl |
CacheControl |
normal, forceRefresh, onlyCache, noCache, noStore |
cacheExpiration |
Duration |
Quick TTL override for this request |
cacheSlidingExpiration |
Duration |
Quick sliding TTL override |
cacheTags |
List<String> |
Tags for this cache entry |
cacheConditional |
bool |
Enable ETag/If-None-Match conditional request |
cacheDedup |
bool |
Enable/disable dedup for this call |
cacheKeyBuilder |
CacheKeyBuilder |
Custom cache key for this request |
cacheSerialize |
SerializeCache |
Custom data serializer |
cacheDeserialize |
DeserializeCache |
Custom data deserializer |
Full override example #
await dio.get('/search', options: Options(extra: {
'perRequestCacheOptions': CachePolicy(
expiration: const Duration(seconds: 30),
slidingExpiration: const Duration(seconds: 10),
tags: ['search', 'volatile'],
serialize: (data) => json.encode(data),
deserialize: (data) => json.decode(data as String),
maxResponseSize: 1024 * 1024,
cacheRequest: true,
cacheResponse: true,
),
}));
Tag-based invalidation — bulk evict by group #
// Assign tags to cache entries
await dio.get('/users/1', options: Options(extra: {'cacheTags': ['user']}));
await dio.get('/users/2', options: Options(extra: {'cacheTags': ['user']}));
// Invalidate everything tagged 'user' with one call
interceptor.invalidateByTag('user');
// Pattern-based invalidation
interceptor.invalidateByPathPattern(RegExp(r'/users/\d+'));
CacheControl modes — 5 strategies per request #
| Mode | Reads cache | Writes cache | Use case |
|---|---|---|---|
normal |
✅ | ✅ | Default behavior |
forceRefresh |
❌ | ✅ | Pull latest data, update cache |
onlyCache |
✅ | ❌ | Offline-first, fail if no cache |
noCache |
❌ | ✅ | Bypass cache but store response |
noStore |
❌ | ❌ | Sensitive data, auth requests |
dio.get('/data', options: Options(extra: {'cacheControl': CacheControl.forceRefresh}));
dio.get('/data', options: Options(extra: {'cacheControl': CacheControl.onlyCache}));
dio.get('/data', options: Options(extra: {'cacheControl': CacheControl.noStore}));
Request deduplication — 1 network call for N concurrent requests #
When 100 widgets request the same endpoint simultaneously, only one hits the network. All others wait and receive the same response.
final dio = Dio()..interceptors.add(DioCacheInterceptor(
options: const DioCacheOptions(enableDeduplication: true),
));
// 5 concurrent calls → 1 network request
final results = await Future.wait([
dio.get('/heavy'),
dio.get('/heavy'),
dio.get('/heavy'),
dio.get('/heavy'),
dio.get('/heavy'),
]);
// All return the same response, only 1 HTTP call made
Disable per-request: extra: {'cacheDedup': false}.
Conditional requests (ETag / If-None-Match) #
Bandwidth saved. Server returns 304 Not Modified → cached body served automatically.
// First request: server returns data + ETag header
await dio.get('/resource');
// Subsequent requests: sends If-None-Match with stored ETag
final r = await dio.get('/resource', options: Options(
extra: {'cacheConditional': true},
));
// Server: 304 → cached response used (zero data transferred)
print(r.extra['fromCache']); // true
Stale-while-revalidate #
Serve stale data instantly from cache while re-fetching in the background. Users see cached data immediately; the UI updates when the fresh response arrives.
final dio = Dio()..interceptors.add(DioCacheInterceptor(
options: const DioCacheOptions(
expiration: const Duration(minutes: 5),
staleWhileRevalidate: const Duration(minutes: 1),
),
));
// After 6 minutes:
// → stale cache returned INSTANTLY (no spinner)
// → network request started in background
// → next request gets fresh data
Serialization / deserialization #
Custom hooks for binary data, protobuf, or any non-JSON format:
await dio.get('/binary', options: Options(extra: {
'perRequestCacheOptions': CachePolicy(
serialize: (data) => (data as Uint8List).toList(), // binary → list
deserialize: (data) => Uint8List.fromList(data as List<int>), // list → binary
),
}));
Full API reference #
DioCacheOptions — global configuration #
| Property | Type | Default | Description |
|---|---|---|---|
expiration |
Duration? |
null | Global cache TTL |
slidingExpiration |
Duration? |
null | Reset TTL on each cache hit |
staleWhileRevalidate |
Duration? |
null | Serve stale + background refresh |
priority |
CacheItemPriority |
normal | Eviction priority |
maxResponseSize |
int? |
null | Max bytes to cache per response |
cacheMethods |
Set<String> |
{'GET'} |
HTTP methods to cache |
autoInvalidateOnMutation |
bool |
true | POST/PUT/DELETE evicts related GETs |
enableDeduplication |
bool |
false | Concurrent request dedup |
policies |
Map<String, CachePolicy> |
{} |
Named cache policies |
keyBuilder |
CacheKeyBuilder? |
null | Custom cache key function |
shouldCacheResponse |
ShouldCacheResponse? |
null | Filter which responses to cache |
shouldCacheRequest |
ShouldCacheRequest? |
null | Filter which requests to cache |
DioCacheInterceptor — instance methods #
| Method | Description |
|---|---|
invalidateByTag(String tag) |
Remove all entries with given tag |
invalidateByPathPattern(RegExp pattern) |
Remove all entries matching URL pattern |
cache |
Access underlying MemoryCache (clear, stats, etc.) |
CachePolicy — per-request override #
| Property | Type | Overrides |
|---|---|---|
expiration |
Duration? |
Absolute TTL |
slidingExpiration |
Duration? |
Sliding TTL |
staleWhileRevalidate |
Duration? |
Stale window |
priority |
CacheItemPriority? |
Eviction priority |
maxResponseSize |
int? |
Max bytes to cache |
tags |
List<String> |
Tags for invalidation |
serialize |
SerializeCache? |
Data serializer |
deserialize |
DeserializeCache? |
Data deserializer |
cacheRequest |
bool? |
Override whether to read from cache |
cacheResponse |
bool? |
Override whether to write to cache |
Integration with openapi_flutter_gen #
Transparent with code generated by openapi_flutter_gen. Add the interceptor
to the Dio instance and all generated API calls are cached automatically:
final client = ApiClient(
baseUrl: 'https://api.example.com',
bearer: BearerSecurity(token: jwt),
dio: Dio()..interceptors.add(DioCacheInterceptor(
options: DioCacheOptions(expiration: const Duration(minutes: 5)),
)),
errorHandler: ApiErrorInterceptor(onUnauthorized: (_) => logout()),
);
// All generated calls are cached transparently
final result = await client.pets.listPets(); // network
final cached = await client.pets.listPets(); // cache hit
Still use per-request control:
final result = await client.users.getUser(
userId: '123',
extra: {'cacheControl': CacheControl.forceRefresh},
);
License #
Dual-licensed.
Open Source — GNU AGPL v3 #
You can use, modify, and distribute this software freely under the terms of the GNU Affero General Public License v3.
Commercial License #
If the AGPL does not fit your business model, a commercial license is available.
Contact us for pricing and terms.