A powerful, zero-boilerplate caching library for Flutter Riverpod.
Riverpod Cache Engine extends Riverpod's Notifier with an automatic, smart caching system that handles offline persistence, in-memory caching, background garbage collection, and seamless data refreshing.
- Zero-Boilerplate Extension: Access caching directly via
caches.write()andcaches.read()inside any Notifier. - Background Garbage Collector: Automatically cleans up expired cache keys without blocking the UI.
- Smart Auto-Refresh: Supports
onExpiredandonNullcallbacks to fetch fresh data automatically when the cache is missing or expired. - In-Memory Cache (Default): Lightning fast RAM caching out of the box if no storage engine is provided.
- Persistent Storage Ready: Easily plug in
Hive,SharedPreferences, or any other storage engine. - Production Ready Logging: Clean, formatted console logs that automatically disable themselves in Release mode.
Add riverpod_cache_engine to your pubspec.yaml:
dependencies:
riverpod_cache_engine: ^1.0.0Call RiverpodCache.initialize() in your main.dart before runApp.
Option A: Zero Config (In-Memory Cache) If you don't provide a storage engine, the library defaults to lightning-fast RAM caching.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await RiverpodCache.initialize(
cleanupInterval: const Duration(hours: 24), // Background GC interval
);
runApp(const ProviderScope(child: MyApp()));
}Option B: Persistent Storage (e.g., Hive)
If you want data to persist after the app closes, pass a CacheStore implementation.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
final box = await Hive.openBox('cache_box');
await RiverpodCache.initialize(
store: HiveCacheEngine(box),
);
runApp(const ProviderScope(child: MyApp()));
}To use any database (Hive, SharedPreferences, Isar, SQLite), simply implement the CacheStore interface. It only requires 3 methods:
import 'package:riverpod_cache/riverpod_cache.dart';
class MyCustomStore implements CacheStore {
final MyDatabase db;
MyCustomStore(this.db);
@override
Future<Map<String, dynamic>?> read(String key) async {
// Read and return the JSON map from your database
return db.get(key);
}
@override
Future<void> write(String key, Map<String, dynamic> data) async {
// Save the JSON map to your database
await db.put(key, data);
}
@override
Future<void> delete(String key) async {
// Delete the key from your database
await db.delete(key);
}
}Any Notifier or AsyncNotifier instantly gains access to the caches controller!
class UserNotifier extends Notifier<UserProfile?> {
@override
UserProfile? build() => null;
Future<void> loadUser() async {
// Read from cache. If not found (onNull) or expired (onExpired),
// the library will automatically call your API, save it, and return the fresh data!
final cachedMap = await caches.read(
'user_profile',
options: CacheOptions(
ttl: const Duration(hours: 1), // Time to live
onNull: () => api.fetchUser(),
onExpired: () => api.fetchUser(),
),
);
if (cachedMap != null) {
state = UserProfile.fromJson(cachedMap);
}
}
Future<void> updateUser(UserProfile newUser) async {
state = newUser;
// Write to cache manually
await caches.write('user_profile', newUser.toJson());
}
}The built-in Garbage Collector doesn't just delete old data; it refreshes it.
If a user leaves the app open and the GC Timer finds an expired key, it will check if you registered an onExpired callback during read() or write(). If found, the GC will call it, fetch fresh data, and update the database silently in the background!
The library prints beautiful, emoji-prefixed logs in Debug Mode:
[RiverpodCache] 🚀 INIT | Starting initialization...
[RiverpodCache] 📖 READ (Miss) | Key: [user_profile] not found. Calling onNull...
[RiverpodCache] ✅ FETCH (onNull) | Successfully fetched and saved key [user_profile]...
[RiverpodCache] 🔄 GC (Timer) | Key: [user_profile] is expired. Calling onExpired...
(Logging disables automatically in Release Mode to protect data and save resources).