Bond Cache¶
Bond Cache provides unified caching with pluggable drivers, object serialization, and computation helpers. Cache anything from simple strings to complex objects with TTL support and automatic cleanup.
Why Bond Cache?¶
Traditional Flutter caching is fragmented and complex:
// ❌ Traditional approach - scattered caching logic
// SharedPreferences for simple data
final prefs = await SharedPreferences.getInstance();
await prefs.setString('user_token', token);
final token = prefs.getString('user_token');
// Custom memory cache for objects
final Map<String, dynamic> _memoryCache = {};
_memoryCache['users'] = users.map((u) => u.toJson()).toList();
// Manual TTL handling
final cacheTime = DateTime.now().millisecondsSinceEpoch;
final expireTime = cacheTime + (5 * 60 * 1000); // 5 minutes
await prefs.setInt('users_expire', expireTime);
// Check expiration manually
final expire = prefs.getInt('users_expire') ?? 0;
if (DateTime.now().millisecondsSinceEpoch > expire) {
// Cache expired, fetch new data
}
Bond Cache unifies all caching needs:
// ✅ Bond Cache approach - unified and simple
// Simple values with TTL
await Cache.put('user_token', token, expiredAfter: Duration(days: 30));
final token = Cache.get<String>('user_token');
// Complex objects with automatic serialization
await Cache.put('users', users, expiredAfter: Duration(minutes: 5));
final users = Cache.get<List<User>>('users');
// Computation caching
final expensiveData = await Cache.remember(
'expensive_computation',
Duration(minutes: 10),
() => performExpensiveOperation(),
);
Quick Start¶
1. Setup in Service Provider¶
// lib/providers/cache_service_provider.dart
class CacheServiceProvider extends ServiceProvider {
@override
Future<void> register(GetIt it) async {
// Configure default cache with SharedPreferences driver
await Cache.configure(
driver: SharedPreferencesCacheDriver(),
defaultExpiration: Duration(hours: 24),
);
// Register additional cache stores
Cache.addStore('memory', InMemoryCacheDriver());
Cache.addStore('persistent', SharedPreferencesCacheDriver());
}
}
2. Basic Caching¶
// Simple key-value caching
await Cache.put('user_name', 'John Doe');
final name = Cache.get<String>('user_name');
// With expiration
await Cache.put(
'session_token',
token,
expiredAfter: Duration(hours: 2),
);
// Check if exists
if (Cache.has('user_preferences')) {
final prefs = Cache.get<UserPreferences>('user_preferences');
}
// Remove from cache
await Cache.remove('old_data');
// Clear all cache
await Cache.clear();
3. Object Caching¶
// Cache complex objects
final user = User(id: '123', name: 'John', email: 'john@example.com');
await Cache.put('current_user', user, expiredAfter: Duration(hours: 1));
// Retrieve with automatic deserialization
final cachedUser = Cache.get<User>('current_user');
// Cache lists
final users = [user1, user2, user3];
await Cache.put('users_list', users, expiredAfter: Duration(minutes: 30));
final cachedUsers = Cache.get<List<User>>('users_list');
Cache Drivers¶
SharedPreferences Driver¶
Best for persistent data that survives app restarts:
// Setup SharedPreferences driver
await Cache.configure(driver: SharedPreferencesCacheDriver());
// Perfect for user preferences, settings, tokens
await Cache.put('user_theme', 'dark');
await Cache.put('notification_enabled', true);
await Cache.put('last_sync', DateTime.now());
In-Memory Driver¶
Best for temporary data and performance-critical operations:
// Setup in-memory driver
Cache.configure(driver: InMemoryCacheDriver());
// Perfect for API responses, computed values, UI state
await Cache.put('api_response', response);
await Cache.put('filtered_data', filteredResults);
await Cache.put('ui_state', currentState);
// Note: Data is lost when app is closed
Multiple Cache Stores¶
Use different drivers for different use cases:
// Configure multiple stores
await Cache.configure(driver: SharedPreferencesCacheDriver()); // Default
Cache.addStore('memory', InMemoryCacheDriver());
Cache.addStore('secure', SecureCacheDriver());
// Use specific stores
await Cache.store('memory').put('temp_data', data);
await Cache.store('secure').put('sensitive_token', token);
await Cache.store('default').put('user_prefs', preferences);
// Or use the default store
await Cache.put('regular_data', data); // Uses default store
Custom Cache Drivers¶
Create your own cache driver for specific needs:
// Custom SQLite cache driver
class SQLiteCacheDriver implements CacheDriver {
late Database _database;
@override
Future<void> initialize() async {
_database = await openDatabase(
'cache.db',
version: 1,
onCreate: (db, version) {
return db.execute('''
CREATE TABLE cache(
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
expires_at INTEGER
)
''');
},
);
}
@override
Future<void> put<T>(String key, T value, {Duration? expiration}) async {
final expiresAt = expiration != null
? DateTime.now().add(expiration).millisecondsSinceEpoch
: null;
await _database.insert(
'cache',
{
'key': key,
'value': jsonEncode(value),
'expires_at': expiresAt,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
@override
Future<T?> get<T>(String key) async {
final results = await _database.query(
'cache',
where: 'key = ?',
whereArgs: [key],
);
if (results.isEmpty) return null;
final row = results.first;
final expiresAt = row['expires_at'] as int?;
// Check expiration
if (expiresAt != null && DateTime.now().millisecondsSinceEpoch > expiresAt) {
await remove(key);
return null;
}
return jsonDecode(row['value'] as String) as T;
}
@override
Future<void> remove(String key) async {
await _database.delete('cache', where: 'key = ?', whereArgs: [key]);
}
@override
Future<void> clear() async {
await _database.delete('cache');
}
@override
Future<bool> has(String key) async {
final results = await _database.query(
'cache',
columns: ['key'],
where: 'key = ?',
whereArgs: [key],
);
return results.isNotEmpty;
}
}
// Register custom driver
Cache.configure(driver: SQLiteCacheDriver());
Computation Caching¶
Cache.remember()¶
Cache expensive computations automatically:
// Expensive API call - cached for 5 minutes
final users = await Cache.remember(
'users_list',
Duration(minutes: 5),
() async {
print('Fetching users from API...'); // Only prints on cache miss
return await api.getUsers();
},
);
// Complex computation - cached for 1 hour
final analytics = await Cache.remember(
'analytics_report',
Duration(hours: 1),
() async {
print('Computing analytics...');
return await computeAnalytics();
},
);
// With parameters in cache key
final userPosts = await Cache.remember(
'user_posts_${userId}',
Duration(minutes: 10),
() => api.getUserPosts(userId),
);
Conditional Caching¶
// Only cache if result meets criteria
final searchResults = await Cache.rememberIf(
'search_${query}',
Duration(minutes: 15),
() => api.search(query),
condition: (results) => results.isNotEmpty, // Only cache non-empty results
);
// Cache with fallback
final userData = await Cache.rememberOrElse(
'user_${userId}',
Duration(minutes: 30),
() => api.getUser(userId),
fallback: () => User.guest(), // Return guest user if API fails
);
Object Serialization¶
Automatic Serialization¶
Bond Cache automatically handles object serialization for classes extending Jsonable:
// Model extending Jsonable
class User extends Jsonable {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'],
name: json['name'],
email: json['email'],
);
@override
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
// Automatic serialization when caching
await Cache.put('user', user); // Automatically calls user.toJson()
final cachedUser = Cache.get<User>('user'); // Automatically calls User.fromJson()
Custom Serialization¶
For classes that don't extend Jsonable:
// Cache with custom serialization
await Cache.putWithFactory(
'custom_object',
customObject,
serializer: (obj) => obj.toMap(),
deserializer: (map) => CustomObject.fromMap(map),
expiredAfter: Duration(minutes: 30),
);
// Retrieve with custom deserialization
final object = await Cache.getWithFactory<CustomObject>(
'custom_object',
deserializer: (map) => CustomObject.fromMap(map),
);
List and Map Caching¶
// Cache lists
final users = [user1, user2, user3];
await Cache.put('users', users);
final cachedUsers = Cache.get<List<User>>('users');
// Cache maps
final userMap = {'123': user1, '456': user2};
await Cache.put('user_map', userMap);
final cachedMap = Cache.get<Map<String, User>>('user_map');
// Cache nested structures
final complexData = {
'users': users,
'settings': userSettings,
'metadata': {'version': '1.0', 'timestamp': DateTime.now()},
};
await Cache.put('complex_data', complexData);
Cache Policies¶
Time-based Expiration¶
// Different expiration times
await Cache.put('short_lived', data, expiredAfter: Duration(minutes: 5));
await Cache.put('medium_lived', data, expiredAfter: Duration(hours: 1));
await Cache.put('long_lived', data, expiredAfter: Duration(days: 7));
// Never expires (until manually removed)
await Cache.put('permanent_data', data); // No expiredAfter
Size-based Eviction¶
// Configure cache with size limits
await Cache.configure(
driver: InMemoryCacheDriver(
maxSize: 100, // Maximum 100 entries
evictionPolicy: EvictionPolicy.lru, // Least Recently Used
),
);
// Available eviction policies
EvictionPolicy.lru // Least Recently Used
EvictionPolicy.lfu // Least Frequently Used
EvictionPolicy.fifo // First In, First Out
EvictionPolicy.random // Random eviction
Cache Warming¶
Pre-populate cache with frequently used data:
// Warm cache on app startup
class CacheWarmingService {
static Future<void> warmCache() async {
// Pre-load user data
final user = await api.getCurrentUser();
await Cache.put('current_user', user, expiredAfter: Duration(hours: 1));
// Pre-load settings
final settings = await api.getUserSettings();
await Cache.put('user_settings', settings, expiredAfter: Duration(days: 1));
// Pre-load frequently accessed data
final categories = await api.getCategories();
await Cache.put('categories', categories, expiredAfter: Duration(hours: 6));
}
}
// Call during app initialization
await CacheWarmingService.warmCache();
Cache Observing¶
Listen to Cache Changes¶
// Listen to all cache changes
Cache.stream().listen((event) {
print('Cache event: ${event.type} for key: ${event.key}');
switch (event.type) {
case CacheEventType.put:
print('Value cached: ${event.value}');
break;
case CacheEventType.get:
print('Value retrieved: ${event.value}');
break;
case CacheEventType.remove:
print('Value removed');
break;
case CacheEventType.clear:
print('Cache cleared');
break;
}
});
// Listen to specific key changes
Cache.streamKey('user_preferences').listen((event) {
if (event.type == CacheEventType.put) {
// User preferences updated
updateUI(event.value as UserPreferences);
}
});
Cache Statistics¶
// Get cache statistics
final stats = await Cache.getStats();
print('Cache hits: ${stats.hits}');
print('Cache misses: ${stats.misses}');
print('Hit ratio: ${stats.hitRatio}%');
print('Total entries: ${stats.entryCount}');
print('Memory usage: ${stats.memoryUsage} bytes');
// Reset statistics
await Cache.resetStats();
Advanced Features¶
Cache Tags¶
Group related cache entries with tags:
// Cache with tags
await Cache.putWithTags('user_123', user, tags: ['user', 'profile']);
await Cache.putWithTags('posts_123', posts, tags: ['user', 'posts']);
await Cache.putWithTags('settings_123', settings, tags: ['user', 'settings']);
// Clear all entries with specific tag
await Cache.clearByTag('user'); // Clears user, posts, and settings
// Get all keys with tag
final userKeys = await Cache.getKeysByTag('user');
Cache Hierarchies¶
Organize cache with hierarchical keys:
// Hierarchical cache keys
await Cache.put('users/123/profile', userProfile);
await Cache.put('users/123/posts', userPosts);
await Cache.put('users/123/settings', userSettings);
await Cache.put('users/456/profile', anotherProfile);
// Clear all user data
await Cache.clearByPrefix('users/123/');
// Get all user-related keys
final userKeys = await Cache.getKeysByPrefix('users/');
Distributed Caching¶
Synchronize cache across multiple app instances:
// Configure distributed cache
await Cache.configure(
driver: DistributedCacheDriver(
localDriver: InMemoryCacheDriver(),
remoteDriver: RedisCacheDriver(),
syncStrategy: SyncStrategy.writeThrough,
),
);
// Strategies
SyncStrategy.writeThrough // Write to both local and remote
SyncStrategy.writeBack // Write to local, sync to remote later
SyncStrategy.readThrough // Read from local, fallback to remote
Integration with Other Packages¶
With BondFire¶
BondFire automatically uses Bond Cache when .cache() is called:
// BondFire automatically caches API responses
final users = await bondFire
.get<ListResponse<User>>('/users')
.factory(ListResponse<User>.fromJson)
.cache(duration: Duration(minutes: 5)) // Uses Bond Cache internally
.execute();
// Custom cache key with BondFire
final user = await bondFire
.get<User>('/users/123')
.factory(User.fromJson)
.cache(
key: 'user_profile_123',
duration: Duration(hours: 1),
)
.execute();
With Bond Analytics¶
Track cache performance:
// Track cache events
class CacheAnalytics {
static void trackCacheHit(String key) {
AppAnalytics.fire(CacheHitEvent(key: key));
}
static void trackCacheMiss(String key) {
AppAnalytics.fire(CacheMissEvent(key: key));
}
static void trackCacheEviction(String key, String reason) {
AppAnalytics.fire(CacheEvictionEvent(key: key, reason: reason));
}
}
// Custom cache driver with analytics
class AnalyticsAwareCacheDriver extends InMemoryCacheDriver {
@override
Future<T?> get<T>(String key) async {
final value = await super.get<T>(key);
if (value != null) {
CacheAnalytics.trackCacheHit(key);
} else {
CacheAnalytics.trackCacheMiss(key);
}
return value;
}
}
Testing¶
Unit Testing Cache¶
void main() {
group('Bond Cache', () {
setUp(() async {
// Use in-memory driver for tests
await Cache.configure(driver: InMemoryCacheDriver());
});
tearDown(() async {
await Cache.clear();
});
test('should store and retrieve values', () async {
await Cache.put('test_key', 'test_value');
final value = Cache.get<String>('test_key');
expect(value, 'test_value');
});
test('should handle expiration', () async {
await Cache.put(
'expiring_key',
'value',
expiredAfter: Duration(milliseconds: 100),
);
// Should exist initially
expect(Cache.has('expiring_key'), true);
// Wait for expiration
await Future.delayed(Duration(milliseconds: 150));
// Should be expired
expect(Cache.has('expiring_key'), false);
});
test('should cache computations', () async {
int callCount = 0;
final computation = () async {
callCount++;
return 'computed_value';
};
// First call should execute computation
final result1 = await Cache.remember('computation', Duration(minutes: 1), computation);
expect(result1, 'computed_value');
expect(callCount, 1);
// Second call should use cache
final result2 = await Cache.remember('computation', Duration(minutes: 1), computation);
expect(result2, 'computed_value');
expect(callCount, 1); // Should not increment
});
});
}
Mock Cache Driver¶
class MockCacheDriver implements CacheDriver {
final Map<String, dynamic> _cache = {};
final Map<String, DateTime> _expiration = {};
@override
Future<void> put<T>(String key, T value, {Duration? expiration}) async {
_cache[key] = value;
if (expiration != null) {
_expiration[key] = DateTime.now().add(expiration);
}
}
@override
Future<T?> get<T>(String key) async {
if (_expiration.containsKey(key)) {
if (DateTime.now().isAfter(_expiration[key]!)) {
_cache.remove(key);
_expiration.remove(key);
return null;
}
}
return _cache[key] as T?;
}
@override
Future<void> remove(String key) async {
_cache.remove(key);
_expiration.remove(key);
}
@override
Future<void> clear() async {
_cache.clear();
_expiration.clear();
}
@override
Future<bool> has(String key) async {
return _cache.containsKey(key);
}
}
// Use in tests
void main() {
group('Service with Cache', () {
late MockCacheDriver mockCache;
late UserService userService;
setUp(() {
mockCache = MockCacheDriver();
Cache.configure(driver: mockCache);
userService = UserService();
});
test('should cache user data', () async {
final user = User(id: '123', name: 'John');
await userService.cacheUser(user);
final cachedUser = await userService.getCachedUser('123');
expect(cachedUser?.name, 'John');
});
});
}
Best Practices¶
✅ Do's¶
// Use descriptive cache keys
await Cache.put('user_profile_${userId}', profile);
await Cache.put('api_response_users_page_${page}', response);
// Set appropriate expiration times
await Cache.put('user_token', token, expiredAfter: Duration(hours: 1));
await Cache.put('app_config', config, expiredAfter: Duration(days: 1));
await Cache.put('temp_data', data, expiredAfter: Duration(minutes: 5));
// Use Cache.remember for expensive operations
final result = await Cache.remember(
'expensive_computation',
Duration(minutes: 10),
() => performExpensiveOperation(),
);
// Choose appropriate drivers
Cache.configure(driver: InMemoryCacheDriver()); // For temporary data
Cache.configure(driver: SharedPreferencesCacheDriver()); // For persistent data
// Handle cache misses gracefully
final user = Cache.get<User>('current_user') ?? User.guest();
❌ Don'ts¶
// Don't use generic cache keys
await Cache.put('data', someData); // ❌ Too generic
await Cache.put('temp', tempValue); // ❌ What kind of temp data?
// Don't cache everything indefinitely
await Cache.put('live_data', data); // ❌ No expiration for live data
// Don't ignore cache failures
Cache.get<User>('user')!.name; // ❌ Might throw null exception
// Don't cache sensitive data without encryption
await Cache.put('password', password); // ❌ Should be encrypted or not cached
// Don't use wrong data types
await Cache.put('user_age', '25'); // ❌ Should be int, not string
await Cache.put('is_admin', 'true'); // ❌ Should be bool, not string
Troubleshooting¶
Common Issues¶
Issue: Cache not persisting between app restarts
// ❌ Problem: Using in-memory driver
Cache.configure(driver: InMemoryCacheDriver());
// ✅ Solution: Use persistent driver
Cache.configure(driver: SharedPreferencesCacheDriver());
Issue: Objects not deserializing correctly
// ❌ Problem: Class doesn't extend Jsonable
class User {
// No fromJson/toJson methods
}
// ✅ Solution: Extend Jsonable or use custom serialization
class User extends Jsonable {
factory User.fromJson(Map<String, dynamic> json) => User(...);
@override
Map<String, dynamic> toJson() => {...};
}
Issue: Cache growing too large
// ❌ Problem: No size limits or expiration
await Cache.put('data_${timestamp}', data); // Keeps growing
// ✅ Solution: Set expiration and size limits
await Cache.put('data', data, expiredAfter: Duration(hours: 1));
Cache.configure(
driver: InMemoryCacheDriver(
maxSize: 1000,
evictionPolicy: EvictionPolicy.lru,
),
);
Next Steps¶
- BondFire Networking - Automatic caching with API calls
- Service Providers - Register cache configuration
- Performance Tips - Optimize cache usage
Bond Cache provides powerful, unified caching for all your Flutter app needs. Start with simple key-value caching and gradually adopt advanced features like computation caching and custom drivers! 🚀