FutureShard includes built-in response caching to reduce network calls and improve app performance. Unlike PersistentShard which persists state across app sessions, response caching stores API responses temporarily in memory or disk to avoid redundant network requests.
| Feature | PersistentShard | Response Caching |
|---|---|---|
| Purpose | Persist app state across sessions | Cache API responses temporarily |
| Storage | Long-term (survives app restarts) | Short-term (cleared on app restart with memory cache) |
| Use Case | User preferences, app settings, todos | API responses, fetched data |
| TTL | No expiration (until cleared) | Configurable expiration time |
Caching is enabled by default in FutureShard. Customize the cache key or expiration:
class UserShard extends FutureShard<User> {
final String userId;
final UserRepository repository;
UserShard({
required this.userId,
required this.repository,
});
// Optional: customize cache key
@override
String get cacheKey => 'user_$userId';
// Optional: customize cache expiration
@override
Duration get cacheTTL => const Duration(hours: 1);
@override
Future<User> build() async {
return await repository.getUser(userId);
}
}
To disable caching, override allowCache:
@override
bool get allowCache => false;
The data is cached directly as-is, without any serialization. This works seamlessly with MemoryCacheService and any cache service that can store Dart objects directly.
FutureShard fetches data, it saves the response to cachecacheTTL duration, cached data is considered stale and fresh data is fetchedrefresh() invalidates the cache and fetches fresh datacacheKey defaults to the shard's runtime type name (runtimeType.toString()). Override it to provide a unique identifier for your cached data:
@override
String get cacheKey => 'product_${productId}_${locale}';
Override cacheKey for parameterized shards. Because the default key is just the type name, every instance of the same shard type shares one cache entry. Two UserShards with different userIds would read and write the same entry and serve each other's data—unless you override cacheKey to include the parameters that distinguish one instance from another.
The default also relies on runtimeType.toString(), which is not stable under release-build obfuscation or tree-shaking. If a shard's cache must survive obfuscation (especially a disk cache), override cacheKey with an explicit literal instead of relying on the type name.
Cached data is considered valid for cacheTTL, which defaults to 1 hour. Override it to set how long cached data should be considered valid:
@override
Duration get cacheTTL => const Duration(minutes: 30); // 30 minutes
By default, FutureShard uses MemoryCacheService (in-memory cache). Provide a custom cache service for persistent caching:
class ProductShard extends FutureShard<Product> {
final CacheService _cacheService;
ProductShard({
required this._cacheService,
required String productId,
}) : _productId = productId;
final String _productId;
@override
bool get allowCache => true;
@override
CacheService get cacheService => _cacheService;
@override
String get cacheKey => 'product_$_productId';
// ... rest of implementation
}
MemoryCacheService stores data in memory. Data is lost when the app restarts:
@override
CacheService get cacheService => MemoryCacheService();
By default MemoryCacheService is unbounded. Pass maxEntries to cap how many entries it retains; once the count exceeds the limit, the oldest entries are evicted on the next write:
// Cap retained entries; oldest are evicted on write past the limit (default: unbounded).
final cache = MemoryCacheService(maxEntries: 100);
cache.entryCount; // entries currently held
final removed = cache.evictExpired(); // drop expired entries on demand; returns count
read still returns expired entries, so stale-while-revalidate flows (CacheMixin's onErrorReturnOldCache) keep working.Create a persistent cache service using SharedPreferences or other storage:
class SharedPrefsCacheService implements CacheService {
final SharedPreferences _prefs;
SharedPrefsCacheService(this._prefs);
@override
Future<void> write(String key, CacheEntry entry) async {
await _prefs.setString(key, jsonEncode(entry.toJson()));
}
@override
Future<CacheEntry?> read(String key) async {
final json = _prefs.getString(key);
if (json == null) return null;
return CacheEntry.fromJson(jsonDecode(json));
}
@override
Future<void> delete(String key) async {
await _prefs.remove(key);
}
@override
Future<void> clearAll() async {
await _prefs.clear();
}
}
You can also use caching directly in your repositories using CacheMixin. This is useful when you want to cache data outside of FutureShard:
import 'package:shard/shard.dart';
class ProductRepository with CacheMixin {
final ProductApi _api;
ProductRepository(this._api);
@override
CacheService get cacheService => MemoryCacheService();
Future<Product> getProduct(String id) {
return resolve<Product>(
key: 'product_$id',
fetcher: () => _api.fetchProduct(id),
ttl: const Duration(hours: 2),
forceRefresh: false,
onErrorReturnOldCache: true, // Fallback to stale data on error
);
}
Future<List<Product>> searchProducts(String query) {
return resolve<List<Product>>(
key: 'products_search_$query',
fetcher: () => _api.searchProducts(query),
ttl: const Duration(minutes: 15),
);
}
// Force refresh a specific product
Future<Product> refreshProduct(String id) {
return resolve<Product>(
key: 'product_$id',
fetcher: () => _api.fetchProduct(id),
forceRefresh: true, // Bypass cache
);
}
}
Note: When using CacheMixin in repositories, the repository's cache and FutureShard's cache are separate. Choose one approach based on your needs:
FutureShard caching for automatic cache management tied to shard lifecycleCacheMixin in repositories for more granular control or when caching outside of shardsCacheMixin can log cache hits, misses, and errors to help you debug caching behavior. Override logCacheEvents to toggle logging, and onCacheLog to redirect log lines to your own logger:
class ProductRepository with CacheMixin {
@override
CacheService get cacheService => MemoryCacheService();
// Toggle cache hit/miss/error logging (defaults to kDebugMode):
@override
bool get logCacheEvents => true;
// Redirect log lines (default sink: dart:developer.log(name: 'shard.cache')):
@override
void onCacheLog(String message) => myLogger.debug(message);
}
Call refresh() to invalidate cache and fetch fresh data:
final userShard = context.read<UserShard>();
userShard.refresh(); // Invalidates cache and fetches fresh data
Keep cache but force a fresh fetch:
userShard.refresh(invalidateCache: false);
Manually delete a cache entry:
final cacheService = MemoryCacheService();
await cacheService.delete('user_123');
class ProductShard extends FutureShard<Product> {
final String productId;
final ProductRepository repository;
final CacheService _cache;
ProductShard({
required this.productId,
required this.repository,
CacheService? cacheService,
}) : _cache = cacheService ?? MemoryCacheService();
@override
bool get allowCache => true;
@override
CacheService get cacheService => _cache;
@override
String get cacheKey => 'product_$productId';
@override
Duration get cacheTTL => const Duration(hours: 2);
@override
Future<Product> build() async {
return await repository.getProduct(productId);
}
}
String get cacheKey => 'user_${userId}_${locale}';
refresh() when user explicitly requests fresh data (pull-to-refresh)✅ Good for:
❌ Not ideal for:
StreamShard instead)Next: Observing a Shard to monitor state changes, errors, and lifecycle events. See also: Persistent Shard.