Dio Cache Plus
Dio Cache Plus is an advanced caching interceptor for Dio β offering per-request control, smart conditional caching, absolute expiry support, dynamic function-based durations, and efficient request deduplication.
π§± Acknowledgments
Originally inspired by flutter-network-sanitizer by Ahmed Elkholy.
Completely reworked and expanded with a modern API, expiry-based caching, dynamic function support, version-safe Hive storage, and advanced request rules.
β¨ Features
β
Conditional Caching β cache only the requests you want.
β
Per-Request Control β customize caching for each call.
β
Smart Expiry β use either a relative Duration or an absolute DateTime expiry.
β
Dynamic Functions β use functions to calculate durations/expiry at runtime.
β
Request Deduplication β identical concurrent requests share the same network response.
β
Persistent Cross-Platform Storage β powered by Hive.
β
Automatic Migration β safe schema updates without crashes.
β
Global or Local Cache Management β total flexibility.
β
Accurate Time-Based Expiry β expiry calculations happen at storage time for precision.
π¦ Installation
Add to your pubspec.yaml:
dependencies:
dio_cache_plus: ^2.0.0
Then, install it by running:
flutter pub get
π Quick Start
Simply add DioCachePlusInterceptor to your Dio instance.
import 'package:dio/dio.dart';
import 'package:dio_cache_plus/dio_cache_plus.dart';
void main() async {
final dio = Dio();
dio.interceptors.add(
DioCachePlusInterceptor(
cacheAll: false,
commonCacheDuration: const Duration(minutes: 5),
isErrorResponse: (response) => response.statusCode != 200,
// β
Conditional rules from constructor
conditionalRules: [
// Cache GET user API responses for 10 minutes
ConditionalCacheRule.duration(
condition: (request) =>
request.method == 'GET' && request.url.contains('/users'),
duration: const Duration(minutes: 10),
),
// Cache GET market data until the market closes (e.g., 3:30 PM)
ConditionalCacheRule.expiry(
condition: (request) =>
request.method == 'GET' && request.url.contains('/market'),
expiry: DateTime.now().copyWith(hour: 15, minute: 30),
),
// Dynamic caching based on time of day
ConditionalCacheRule.durationFn(
condition: (request) =>
request.method == 'GET' && request.url.contains('/news'),
durationFn: () {
final hour = DateTime.now().hour;
// Cache longer during off-peak hours
return hour >= 22 || hour < 6
? const Duration(hours: 4)
: const Duration(minutes: 30);
},
),
],
),
);
// Not cached
await dio.get('/api/news');
// Cached for 5 minutes (default duration)
final cached = await dio.get(
'/api/users',
options: Options().setCaching(enableCache: true),
);
}
π οΈ Usage Examples
Per-Request Control
Use type-safe methods for different caching strategies:
// Cache GET requests with static duration
final response1 = await dio.get(
'/api/big_data',
options: Options().setCachingWithDuration(
enableCache: true,
duration: const Duration(hours: 2),
),
);
// Cache with dynamic duration function
final response2 = await dio.get(
'/api/dynamic_data',
options: Options().setCachingWithDurationFn(
enableCache: true,
durationFn: () {
final hour = DateTime.now().hour;
return hour >= 22 || hour < 6
? const Duration(hours: 4)
: const Duration(minutes: 30);
},
),
);
// Cache with static expiry
final response3 = await dio.get(
'/api/market_data',
options: Options().setCachingWithExpiry(
enableCache: true,
expiry: DateTime.now().copyWith(hour: 16, minute: 0),
),
);
// Cache with dynamic expiry function
final response4 = await dio.get(
'/api/reports',
options: Options().setCachingWithExpiryFn(
enableCache: true,
expiryFn: () => DateTime.now().add(const Duration(days: 1)),
),
);
// Simple caching (uses global default duration)
final response5 = await dio.get(
'/api/data',
options: Options().setCaching(enableCache: true),
);
// Disable cache even if global cacheAll=true
final noCache = await dio.get(
'/api/live_feed',
options: Options().setCaching(enableCache: false),
);
Dynamic Duration Functions
Calculate cache durations dynamically at runtime:
// Weekend-aware caching
final weekendResponse = await dio.get(
'/api/weekly-report',
options: Options().setCachingWithDurationFn(
enableCache: true,
durationFn: () {
final isWeekend = DateTime.now().weekday >= 6; // Saturday or Sunday
return isWeekend
? const Duration(days: 3) // Longer cache on weekends
: const Duration(hours: 12); // Shorter cache on weekdays
},
),
);
// User-based caching strategies
final userResponse = await dio.get(
'/api/user/profile',
options: Options().setCachingWithDurationFn(
enableCache: true,
durationFn: () {
final userType = authService.currentUser?.type;
switch (userType) {
case UserType.premium:
return const Duration(hours: 4);
case UserType.standard:
return const Duration(hours: 1);
case UserType.guest:
default:
return const Duration(minutes: 15);
}
},
),
);
Absolute Expiry with Dynamic Functions
Calculate expiry times dynamically based on business logic:
// Cache until next market close (dynamic calculation)
final marketResponse = await dio.get(
'/api/market-data',
options: Options().setCachingWithExpiryFn(
enableCache: true,
expiryFn: () {
final now = DateTime.now();
// If it's after 4 PM, cache until next day 4 PM
if (now.hour >= 16) {
return DateTime(now.year, now.month, now.day + 1, 16, 0);
} else {
return DateTime(now.year, now.month, now.day, 16, 0);
}
},
),
);
// Cache until top of next hour
final topOfHourResponse = await dio.get(
'/api/hourly-data',
options: Options().setCachingWithExpiryFn(
enableCache: true,
expiryFn: () {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, now.hour + 1);
},
),
);
Force Refresh
Bypass the cache to get fresh data from the server:
// This request will ignore any cached data and hit the network
final freshResponse = await dio.get(
'/api/users/123',
options: Options().setCachingWithDuration(
enableCache: true,
duration: const Duration(minutes: 30),
invalidateCache: true, // Forces network request
),
);
Request Deduplication
Concurrent identical GET requests are automatically merged:
final futures = List.generate(
10,
(_) => dio.get('/api/trending'),
);
final responses = await Future.wait(futures);
// Only 1 actual network request was made π―
π§ Runtime Rule Management
Add or remove conditional caching rules dynamically at runtime:
// Add rule with static duration
DioCachePlusInterceptor.addConditionalCaching(
'user_cache',
ConditionalCacheRule.duration(
condition: (request) =>
request.method == 'GET' && request.url.contains('/users'),
duration: const Duration(minutes: 30),
),
);
// Add rule with dynamic duration function
DioCachePlusInterceptor.addConditionalCaching(
'news_cache',
ConditionalCacheRule.durationFn(
condition: (request) =>
request.method == 'GET' && request.url.contains('/news'),
durationFn: () {
final hour = DateTime.now().hour;
return hour >= 22 || hour < 6
? const Duration(hours: 4) // Longer cache at night
: const Duration(minutes: 30); // Shorter cache during day
},
),
);
// Add rule with static expiry
DioCachePlusInterceptor.addConditionalCaching(
'market_cache',
ConditionalCacheRule.expiry(
condition: (request) =>
request.method == 'GET' && request.url.contains('/market'),
expiry: DateTime.now().copyWith(hour: 16, minute: 0), // Until 4 PM today
),
);
// Add rule with dynamic expiry function
DioCachePlusInterceptor.addConditionalCaching(
'report_cache',
ConditionalCacheRule.expiryFn(
condition: (request) =>
request.method == 'GET' && request.url.contains('/reports'),
expiryFn: () => DateTime.now().add(const Duration(days: 1)), // Until same time tomorrow
),
);
// Add rule without specific timing (uses global default)
DioCachePlusInterceptor.addConditionalCaching(
'api_cache',
ConditionalCacheRule.conditionalOnly(
condition: (request) =>
request.method == 'GET' && request.url.contains('/api'),
),
);
// Remove a rule
DioCachePlusInterceptor.removeConditionalCaching('user_cache');
// Remove cached data based on condition
DioCachePlusInterceptor.removeConditionalCachingData(
(request) => request.method == 'GET' && request.url.contains('/users'),
);
// Clear all cached data (but keep rules)
await DioCachePlusInterceptor.clearAll();
βοΈ How It Works
Caching Flow
- Request Interception: When a request is made, a unique key is generated based on method, URL, and parameters.
- Cache Check: The interceptor checks if caching is enabled via global config, per-request options, or matching conditional rules.
- Expiry Validation: If cached data exists, its timestamp is checked against the configured duration/expiry.
- Network Fallback: If no valid cache exists, the request proceeds to the network.
- Storage: Successful responses are stored with precise expiry calculation at storage time.
Smart Expiry Calculation
Unlike other caching solutions, Dio Cache Plus calculates expiry durations at the moment of storage, not when the request is configured. This ensures:
- Market closing times expire exactly at the specified time
- Time-sensitive data respects absolute deadlines
- Dynamic functions are executed at cache time for fresh values
- No timing drift between request configuration and actual caching
Function Execution Precedence
When multiple duration/expiry options are provided, they are evaluated in this order:
expiryFn- Dynamic expiry functionexpiry- Static expiry DateTimedurationFn- Dynamic duration functionduration- Static duration- Conditional rule functions
- Global default duration
Deduplication
- The interceptor tracks all outgoing network requests
- Identical concurrent requests are queued instead of creating new network calls
- All queued requests receive the same result when the original completes
Cache Invalidation
- Force Refresh:
invalidateCache: trueremoves existing cache before making the network request - Automatic Expiration: Entries expire based on their configured duration or absolute expiry
- Conditional Data Removal:
removeConditionalCachingData()removes cached data matching specific patterns - Global Clear:
clearAll()wipes the entire cache
π API Reference
DioCachePlusInterceptor
The main interceptor class.
| Constructor Parameter | Type | Required | Description |
|---|---|---|---|
cacheAll |
bool |
Yes | When true, caches all requests by default. When false, requires explicit opt-in via setCaching() |
commonCacheDuration |
Duration |
Yes | Default cache duration when no specific duration is provided |
isErrorResponse |
bool Function(Response) |
Yes | Predicate to determine if a response represents an error (prevents caching of errors) |
conditionalRules |
List<ConditionalCacheRule>? |
No | List of conditional caching rules applied at interceptor creation |
Static Methods:
addConditionalCaching(String key, ConditionalCacheRule rule)- Adds a conditional caching ruleremoveConditionalCaching(String key)- Removes a conditional ruleremoveConditionalCachingData(RequestMatcher condition)- Removes cached data matching the conditionclearAll()- Clears all cached data
CacheOptionsExtension
Per-request cache control via Options extension methods:
| Method | Parameters | Description |
|---|---|---|
setCachingWithDuration |
enableCache, duration, overrideConditionalCache, invalidateCache |
Caching with static duration |
setCachingWithDurationFn |
enableCache, durationFn, overrideConditionalCache, invalidateCache |
Caching with dynamic duration function |
setCachingWithExpiry |
enableCache, expiry, overrideConditionalCache, invalidateCache |
Caching with static expiry DateTime |
setCachingWithExpiryFn |
enableCache, expiryFn, overrideConditionalCache, invalidateCache |
Caching with dynamic expiry function |
setCaching |
enableCache, overrideConditionalCache, invalidateCache |
Simple caching (uses global default) |
ConditionalCacheRule Factory Constructors
Define conditional caching rules with precise expiry control:
| Constructor | Parameters | Description |
|---|---|---|
duration |
condition, duration |
Rule with static duration |
durationFn |
condition, durationFn |
Rule with dynamic duration function |
expiry |
condition, expiry |
Rule with static expiry DateTime |
expiryFn |
condition, expiryFn |
Rule with dynamic expiry function |
conditionalOnly |
condition |
Rule without timing (uses global default) |
RequestDetails Object
The RequestDetails object passed to condition functions contains:
| Property | Type | Description |
|---|---|---|
method |
String |
HTTP method (GET, POST, etc.) |
url |
String |
Full request URL |
queryParameters |
Map<String, dynamic> |
Request query parameters |
π― Advanced Usage
Mixed Caching Strategies
// Combine static and dynamic caching rules
dio.interceptors.add(
DioCachePlusInterceptor(
cacheAll: false,
commonCacheDuration: const Duration(minutes: 10),
isErrorResponse: (r) => r.statusCode != 200,
conditionalRules: [
// GET User data with dynamic duration
ConditionalCacheRule.durationFn(
condition: (request) =>
request.method == 'GET' && request.url.contains('/users'),
durationFn: () {
final isWeekend = DateTime.now().weekday >= 6;
return isWeekend
? const Duration(hours: 6)
: const Duration(hours: 2);
},
),
// GET News articles cached until end of day
ConditionalCacheRule.expiryFn(
condition: (request) =>
request.method == 'GET' && request.url.contains('/news'),
expiryFn: () {
final now = DateTime.now();
return DateTime(now.year, now.month, now.day, 23, 59, 59);
},
),
],
),
);
Custom Cache Key Generation
Override the default cache key generation for specific requests:
final response = await dio.get(
'/api/data',
options: Options(extra: {
'generatedRequestKey': 'custom_key_123', // Custom cache key
}).setCachingWithDuration(
enableCache: true,
duration: const Duration(hours: 2),
),
);
π Platform Support
| Platform | Status |
|---|---|
| Android | β |
| iOS | β |
| Web | β |
| Windows | β |
| macOS | β |
| Linux | β |
π Performance Benefits
- Reduced Network Calls: Caching and deduplication cut redundant requests by up to 90%
- Faster Response Times: Local cache serves data instantaneously
- Lower Bandwidth Usage: Optimized for slow networks and metered connections
- Precise Expiry Control: Time-sensitive data respects exact deadlines
- Dynamic Optimization: Cache behavior adapts to runtime conditions
- Server Load Reduction: Protect backend services from unnecessary traffic
- Improved User Experience: Apps feel faster and more responsive
π§ Troubleshooting
Cache Not Working?
- Ensure
enableCache: trueis set insetCaching()orcacheAll: truein interceptor - Check that your
isErrorResponsefunction correctly identifies successful responses - Verify conditional rule conditions match your requests (remember to check method == 'GET')
Expiry Not Respected?
- Use
expiryorexpiryFninstead ofdurationfor absolute time boundaries - Expiry is calculated at storage time, so it's always accurate
- Check that your DateTime includes timezone information if needed
Function Errors?
- Wrap your durationFn/expiryFn in try-catch blocks if they might throw exceptions
- Functions are executed at cache time, not configuration time
- Ensure functions don't have side effects that could cause issues
Memory Issues?
- The interceptor automatically cleans up completed requests
- Hive provides efficient disk-based storage
- Use
clearAll()periodically if needed
π€ Contributing
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
π License
This project is licensed under the MIT License - see the LICENSE file for details.
Dio Cache Plus - Smart, dynamic, precise caching for Flutter apps. β‘
Libraries
- dio_cache_plus
- Dio Cache Plus is an enhanced caching interceptor for Dio.