Ecache
A simple, flexible, and powerful caching library for Dart and Flutter, designed to be easy to use while providing a robust set of features for managing cached data.
Features
- Multiple Caching Strategies: Choose from several built-in eviction policies:
- LRU (Least Recently Used): Evicts the least recently accessed items first.
- LFU (Least Frequently Used): Evicts the least frequently accessed items first.
- FIFO (First-In, First-Out): A simple strategy that evicts the oldest items first.
- Expiration-based: Evicts items that have passed their expiration time.
- Pluggable Architecture: The library is designed with a decoupled architecture, allowing you to mix and match components or create your own.
- Asynchronous Value Production (Write-through cache): Automatically fetch and cache values that are expensive to compute or retrieve, ensuring the production logic runs only once for a given key.
- Detailed Statistics: Monitor cache performance with built-in statistics tracking for hits, misses, and evictions.
- Extensible Storage: While a simple
Map
-based storage is provided, you can create your own storage solutions (e.g., for disk-based or database-backed caching). - Null-Safe and Well-Documented: The entire API is null-safe and comes with comprehensive documentation.
Getting Started
To use this library in your project, add it to your pubspec.yaml
file:
dependencies:
ecache: ^latest
Then, run pub get
or flutter pub get
.
Usage
Creating a Simple Cache (FIFO)
This is the most basic cache, which removes the oldest entry when the capacity is reached.
import 'package:ecache/ecache.dart';
void main() {
// Create a cache with a capacity of 10
final cache = SimpleCache<String, int>(capacity: 10);
// Set and get values
cache.set('a', 1);
final value = cache.get('a'); // returns 1
print('Value for key "a": $value');
}
Using a Least Recently Used (LRU) Cache
This cache is ideal when you want to keep the most recently accessed items.
import 'package:ecache/ecache.dart';
void main() {
final cache = LruCache<String, String>(capacity: 2);
cache.set('user:1', 'Alice');
cache.set('user:2', 'Bob');
// Accessing 'user:1' makes it the most recently used
cache.get('user:1');
// Adding a new item will evict the least recently used ('user:2')
cache.set('user:3', 'Charlie');
print(cache.containsKey('user:2')); // false
}
Caching with an Expiration Time
Set a default duration for all entries in the cache.
import 'package:ecache/ecache.dart';
void main() async {
final cache = ExpirationCache<String, String>(
capacity: 10,
duration: const Duration(seconds: 5),
);
cache.set('session', 'active');
print(cache.get('session')); // 'active'
// Wait for the entry to expire
await Future.delayed(const Duration(seconds: 6));
print(cache.get('session')); // null
}
Eviction with Cleanup
import 'package:ecache/ecache.dart';
void main() {
final cache = SimpleCache(
capacity: 20,
onEvict: (key, value) {
value.dispose(); // Clean up evicted items
},
);
cache['key'] = 42;
cache['halfKey'] = 21;
}
Get Statistics
Enable statistics collection to monitor cache performance, detect memory leaks, and optimize cache configurations:
import 'package:ecache/ecache.dart';
void main() {
// Enable statistics collection (only works in debug mode)
StorageMgr().setEnabled(true);
final userCache = SimpleCache<String, User>(capacity: 100);
final sessionCache = SimpleCache<String, Session>(capacity: 50);
// Perform some cache operations
userCache.set('user1', User('Alice'));
userCache.get('user1'); // hit
userCache.get('user999'); // miss
sessionCache.set('session1', Session('abc123'));
// View individual cache statistics
print(userCache.storage); // Shows hits, misses, evictions for this cache
// View comprehensive report for all caches
print(StorageMgr().createReport());
}
What you can accomplish with statistics:
- Find Memory Leaks: Detect undisposed caches that continue to consume memory
- Optimize Capacity: Analyze hit/miss ratios to determine if cache sizes are appropriate
- Monitor Performance: Track cache effectiveness across your application
- Debug Issues: Identify caches with unexpected eviction patterns
- Resource Planning: Understand memory usage patterns for different cache types
Key Metrics Available:
hits/misses
: Cache effectiveness ratiocurrentEntries/maxEntries
: Current vs peak memory usagecapacity
: Configured maximum entriesevictions
: How often items are removed due to capacity limitssets
: Total number of items stored
Example Report Output:
Storage Report (2024-01-15T10:30:45.123Z)
Storages registered: 2, unregistered: 0
1, StatisticsStorage<String, User>: capacity: 100, maxEntries: 45, currentEntries: 42, hits: 156, misses: 23, evictions: 3, sets: 48
2, StatisticsStorage<String, Session>: capacity: 50, maxEntries: 12, currentEntries: 12, hits: 89, misses: 5, evictions: 0, sets: 12
⚠️ Statistics collection only works in debug mode and has minimal performance impact.
Weak References
import 'package:ecache/ecache.dart';
void main() {
final storage = WeakReferenceStorage();
final cache = SimpleCache(storage: storage, capacity: 20);
}
⚠️ Weak references allow garbage collection of older items beyond the guaranteed capacity.
- Do not use eviction callbacks and weak storage together — callbacks may not fire when GC clears items.
- WeakReferenceStorage are not able to produce statistics
Asynchronous Value Production (Write-through cache)
Use getOrProduce
to fetch and cache data from a database or a network API while making sure that multiple calls will fetch the data only once and all calls receive the same instance of the produced data.
import 'package:ecache/ecache.dart';
// A function that simulates fetching data from a network
Future<String> fetchUserData(String userId) async {
print('Fetching data for $userId...');
await Future.delayed(const Duration(seconds: 2)); // Simulate network latency
return 'User data for $userId';
}
void main() async {
final cache = SimpleCache<String, String>(capacity: 5);
// The first call will trigger the fetchUserData function
final data1 = await cache.getOrProduce('user:123', fetchUserData);
print(data1);
// The second call will return the cached data instantly
final data2 = await cache.getOrProduce('user:123', fetchUserData);
print(data2);
}
Architecture
The library is built on three core components:
Cache
: The main interface that developers interact with. Concrete implementations likeSimpleCache
,LruCache
, andLfuCache
provide the caching logic.Storage
: An abstraction for the underlying key-value store. The default isSimpleStorage
, which uses aLinkedHashMap
, but custom implementations can be created.Strategy
: The logic that governs how and when entries are evicted from the cache. Each cache type uses a corresponding strategy (e.g.,LruStrategy
).
This decoupled design allows for great flexibility in composing caches that fit specific needs.
Contributing
Contributions are welcome! Please feel free to open an issue or submit a pull request.
License
This library is licensed under the MIT License. See the LICENSE
file for details.
Libraries
- ecache
- A simple and flexible caching library for Dart.