dio_mcache 0.1.3 copy "dio_mcache: ^0.1.3" to clipboard
dio_mcache: ^0.1.3 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.

pub.dev CI Publish Stars License tests license

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.

0
likes
130
points
262
downloads

Documentation

API reference

Publisher

verified publisherpurplesoft.io

Weekly Downloads

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-while-revalidate, and full control over every caching behavior at global and per-request level.

Repository (GitHub)
View/report issues

Topics

#dio #cache #caching #http #flutter

License

AGPL-3.0 (license)

Dependencies

dio, mcache_dart

More

Packages that depend on dio_mcache