Cache
Magic provides an expressive, unified API for various caching backends, with built-in event dispatching so you can react to cache reads, writes, and removals.
On this page
Introduction#
Magic provides an expressive, unified API for various caching backends. The framework ships with a native FileStore driver that supports both Mobile/Desktop (File System) and Web (LocalStorage).
Caching is essential for reducing API calls, storing computed values, and improving application performance.
Configuration#
Enabling Cache Support#
Add CacheServiceProvider to your providers in config/app.dart:
'providers': [
(app) => CacheServiceProvider(app),
// ... other providers
],
Cache Configuration#
Create lib/config/cache.dart:
import 'package:magic/magic.dart';
Map<String, dynamic> get cacheConfig => {
'cache': {
'driver': FileStore(fileName: 'magic_cache'),
'ttl': 3600, // Default TTL in seconds
},
};
Retrieving Items#
Use the Cache facade to retrieve items from the cache:
// Basic retrieval
final value = Cache.get('key');
// With default value
final username = Cache.get('username', defaultValue: 'Guest');
// Check existence
if (Cache.has('user_settings')) {
final settings = Cache.get('user_settings');
}
Cache.get() and Cache.has() are synchronous. The write methods (put, forget, flush) and remember return a Future.
Storing Items#
Store items with optional TTL (time-to-live):
// With custom TTL
await Cache.put('key', 'value', ttl: Duration(minutes: 10));
// With default TTL from config
await Cache.put('key', 'value');
// Store any serializable data
await Cache.put('user', {
'id': 1,
'name': 'John',
'email': '[email protected]',
});
The Remember Method#
Cache.remember() retrieves an item from the cache, or computes and stores it:
final users = await Cache.remember(
'users',
Duration(minutes: 5),
() async {
final response = await Http.get('/users');
return response.data;
},
);
This method:
- Checks if
usersexists in cache - If yes, returns the cached value
- If no, calls the closure, stores the result, and returns it
API Response Caching#
class UserController extends MagicController {
Future<List<User>> getUsers() async {
return await Cache.remember('all_users', Duration(minutes: 10), () async {
final response = await Http.get('/users');
return (response.data as List).map((u) => User.fromMap(u)).toList();
});
}
}
Computed Values#
final dashboardStats = await Cache.remember('dashboard_stats', Duration(hours: 1), () async {
final response = await Http.get('/dashboard/stats');
return response.data;
});
Removing Items#
// Remove a single item
await Cache.forget('users');
// Clear entire cache
await Cache.flush();
Invalidating Related Cache#
class UserController extends MagicController {
Future<void> updateUser(Map<String, dynamic> data) async {
final response = await Http.put('/users/${data['id']}', data: data);
if (response.successful) {
// Invalidate related caches
await Cache.forget('all_users');
await Cache.forget('user_${data['id']}');
await Cache.forget('dashboard_stats');
}
}
}
Custom Cache Drivers#
Implement CacheStore for custom backends. get() and has() are synchronous in the contract, so a driver that talks to a remote backend keeps a local copy and loads it in init(), as FileStore does:
class MemoryStore implements CacheStore {
final Map<String, dynamic> _items = {};
final Map<String, DateTime> _expiresAt = {};
@override
Future<void> init() async {}
@override
dynamic get(String key, {dynamic defaultValue}) {
return has(key) ? _items[key] : defaultValue;
}
@override
Future<void> put(String key, dynamic value, {Duration? ttl}) async {
_items[key] = value;
// A null ttl means the item never expires.
if (ttl == null) {
_expiresAt.remove(key);
} else {
_expiresAt[key] = DateTime.now().add(ttl);
}
}
@override
bool has(String key) {
final expiresAt = _expiresAt[key];
if (expiresAt != null && !DateTime.now().isBefore(expiresAt)) {
_items.remove(key);
_expiresAt.remove(key);
}
return _items.containsKey(key);
}
@override
Future<void> forget(String key) async {
_items.remove(key);
_expiresAt.remove(key);
}
@override
Future<void> flush() async {
_items.clear();
_expiresAt.clear();
}
}
Use your custom driver in config:
'cache': {
'driver': MemoryStore(),
},
Cache Events and Listeners#
Magic dispatches events on every cache operation. You can listen to these events via your AppEventServiceProvider to add logging, metrics, or invalidation side-effects without coupling that logic to call sites.
Event Classes#
All cache events extend MagicEvent and live in package:magic/magic.dart:
| Event | Payload | Fired when |
|---|---|---|
CacheHit |
key, value |
A Cache.get() returns a stored value. |
CacheMiss |
key |
A Cache.get() finds no stored value. |
CachePut |
key, value, ttl? |
A value is written via Cache.put() or Cache.remember(). |
CacheForget |
key |
A single key is removed via Cache.forget(). |
CacheFlush |
(none) | The entire cache is cleared via Cache.flush(). |
Listening to Cache Events#
Register listeners in your AppEventServiceProvider:
import 'package:magic/magic.dart';
class LogCacheHit extends MagicListener<CacheHit> {
@override
Future<void> handle(CacheHit event) async {
Log.debug('Cache hit: ${event.key}');
}
}
class LogCacheMiss extends MagicListener<CacheMiss> {
@override
Future<void> handle(CacheMiss event) async {
Log.debug('Cache miss: ${event.key}');
}
}
Then wire them in your event service provider:
import 'package:magic/magic.dart';
class AppEventServiceProvider extends BaseEventServiceProvider {
AppEventServiceProvider(super.app);
@override
Map<Type, List<MagicListener Function()>> get listen => {
CacheHit: [() => LogCacheHit()],
CacheMiss: [() => LogCacheMiss()],
CachePut: [() => RecordCacheWrite()],
CacheForget: [() => RecordCacheRemoval()],
CacheFlush: [() => RecordCacheFlush()],
};
}
The separate magic_devtools package can observe these events during development; core does not depend on it.
Testing#
Use Cache.fake() to replace the real cache manager with an in-memory FakeCacheManager during tests. The fake supports assertions and records every operation.
import 'package:flutter_test/flutter_test.dart';
import 'package:magic/magic.dart';
import 'package:magic/testing.dart';
void main() {
setUp(() {
MagicApp.reset();
Magic.flush();
});
test('caches users after first fetch', () async {
final fake = Cache.fake();
await Cache.put('users', ['alice', 'bob']);
fake.assertHas('users');
fake.assertPut('users');
});
test('cleans up after test', () async {
final fake = Cache.fake();
fake.reset(); // clears store and recorded operations
Cache.unfake();
});
}
FakeCacheManager exposes:
assertHas(key): asserts the key currently exists in the store.assertMissing(key): asserts the key is absent.assertPut(key): asserts the key was stored at least once viaput.recorded: the full chronological list of cache operations for manual inspection.reset(): clears the store and the recorded list between assertions.