dio_cache_plus 2.0.3
dio_cache_plus: ^2.0.3 copied to clipboard
Enhanced Dio caching interceptor with per-request control, conditional caching, and expiration.
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. β‘