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.
- Introduction
- Configuration
- Retrieving Items
- Storing Items
- The Remember Method
- Removing Items
- Custom Cache Drivers
- Cache Events and Listeners
- Testing
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 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 = await Cache.get('key');
// With default value
final username = await Cache.get('username', defaultValue: 'Guest');
// Check existence
if (await Cache.has('user_settings')) {
final settings = await Cache.get('user_settings');
}
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
The most powerful caching pattern—retrieve from cache or compute and store:
final users = await Cache.remember(
'users',
Duration(minutes: 5),
() async {
return await Http.get('/users');
},
);
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> getUsers() async {
return await Cache.remember('all_users', Duration(minutes: 10), () async {
final response = await Http.get('/users');
return (response.body as List).map((u) => User.fromMap(u)).toList();
});
}
}
Computed Values
final dashboardStats = await Cache.remember('dashboard_stats', Duration(hours: 1), () async {
return {
'total_users': await User.count(),
'active_monitors': await Monitor.where('is_active', true).count(),
'incidents_today': await Incident.whereDate('created_at', Carbon.today()).count(),
};
});
Removing Items
// Remove a single item
await Cache.forget('users');
// Clear entire cache
await Cache.flush();
Invalidating Related Cache
class UserController extends MagicController {
Future updateUser(Map 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:
class RedisStore implements CacheStore {
final RedisClient _client;
RedisStore(this._client);
@override
Future init() async {
await _client.connect();
}
@override
Future get(String key, {dynamic defaultValue}) async {
final value = await _client.get(key);
if (value == null) return defaultValue;
return jsonDecode(value);
}
@override
Future put(String key, dynamic value, {Duration? ttl}) async {
final encoded = jsonEncode(value);
if (ttl != null) {
await _client.setex(key, ttl.inSeconds, encoded);
} else {
await _client.set(key, encoded);
}
}
@override
Future has(String key) async {
return await _client.exists(key) > 0;
}
@override
Future forget(String key) async {
await _client.del(key);
}
@override
Future flush() async {
await _client.flushdb();
}
}
Use your custom driver in config:
'cache': {
'driver': RedisStore(RedisClient()),
},
[!TIP] Use caching for expensive API calls, complex computations, and data that doesn't change frequently.
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 {
@override
Future handle(CacheHit event) async {
Log.debug('Cache hit: ${event.key}');
}
}
class LogCacheMiss extends MagicListener {
@override
Future 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 EventServiceProvider {
AppEventServiceProvider(super.app);
@override
Map> get listen => {
CacheHit: [() => LogCacheHit()],
CacheMiss: [() => LogCacheMiss()],
CachePut: [() => RecordCacheWrite()],
CacheForget: [() => RecordCacheRemoval()],
CacheFlush: [() => RecordCacheFlush()],
};
}
Telescope Integration
When magic_devtools is installed, the MagicCacheWatcher registers listeners for all five cache events automatically and streams them into Telescope's ring buffer, where they appear as cache entries readable by AI agents and the telescope:tail command. No configuration is required on your side; Telescope observes passively without a write path.
See Magic DevTools for installation and setup.
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.