hope_cache 0.1.3
hope_cache: ^0.1.3 copied to clipboard
Simple and fast caching for Dart/Flutter with TTL support, multiple eviction strategies, and pluggable storage.
Hope Cache #
Caching and data-fetching for Flutter — TTL, eviction, and a reactive widget layer.
Features #
- Zero dependencies — pure Dart + Flutter, nothing else
- No wrapper widget — just
HopeClient.init()in main, noQueryClientProviderneeded - Own your cache —
CacheManagerworks standalone without the query layer - Pluggable storage — bring your own storage backend
- Eager cleanup — controllers disposed immediately when no widgets are listening
- LRU, LFU, FIFO eviction — with TTL, global default and per-key overrides
- Map keys, batch operations, pattern invalidation
HopeBuilder— reactive data fetching with loading, error, refetchHopeMutationBuilder— write operations with callbacks- Infinite scroll — just add
getNextPageParam, no extra setup invalidatePrefix— invalidate a group of related queries in one call- Stale-while-revalidate, retry, polling, dependent queries
- Optimistic updates, prefetch, invalidation
Installation #
dart pub add hope_cache
Setup #
Cache only
final cache = await CacheManager.create(
maxSize: 1024 * 1024,
defaultTTL: Duration(minutes: 5),
evictionPolicy: EvictionPolicy.lru,
);
Cache + Query
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await HopeClient.init(
maxSize: 1024 * 1024,
defaultTTL: Duration(minutes: 5),
evictionPolicy: EvictionPolicy.lru,
);
runApp(MyApp());
}
HopeClient.init() sets up the cache internally — no need to create CacheManager separately.
Cache #
await cache.set('user_123', {'name': 'Alice'});
final user = await cache.getIfPresent('user_123');
Map Keys
await cache.set(
{'resource': 'product', 'id': '123'},
{'name': 'Laptop'},
);
// different order = same key
final product = await cache.getIfPresent(
{'id': '123', 'resource': 'product'},
);
Query #
HopeBuilder<List<User>>(
queryKey: ['users'],
fetcher: (_) => api.getUsers(),
builder: (context, state) {
if (state.isLoading) return CircularProgressIndicator();
if (state.isError) return Text(state.error.toString());
return UserList(state.data!);
},
)
Dependent Query
HopeBuilder<List<Post>>(
queryKey: ['posts', userId],
fetcher: (_) => api.getPosts(userId),
options: HopeOptions(enabled: userId != null),
builder: (context, state) {
if (state.isIdle) return Text('No user selected');
if (state.isLoading) return CircularProgressIndicator();
if (state.isError) return Text(state.error.toString());
return PostList(state.data!);
},
)
Stale Time
HopeBuilder<List<User>>(
queryKey: ['users'],
fetcher: (_) => api.getUsers(),
options: HopeOptions(staleTime: Duration(minutes: 2)),
builder: (context, state) => UserList(state.data ?? []),
)
Polling
HopeBuilder<Stats>(
queryKey: ['stats'],
fetcher: (_) => api.getStats(),
options: HopeOptions(refetchInterval: Duration(seconds: 30)),
builder: (context, state) => StatsWidget(state.data),
)
Refetch on Resume
HopeBuilder<List<User>>(
queryKey: ['users'],
fetcher: (_) => api.getUsers(),
options: HopeOptions(
refetchOnResume: true,
staleTime: Duration(minutes: 5),
),
builder: (context, state) => UserList(state.data ?? []),
)
Manual Refetch
HopeBuilder<List<User>>(
queryKey: ['users'],
fetcher: (_) => api.getUsers(),
builder: (context, state) {
return Column(
children: [
UserList(state.data ?? []),
ElevatedButton(
onPressed: state.refetch,
child: Text('Refresh'),
),
],
);
},
)
Infinite Scroll #
HopeBuilder<UserPage>(
queryKey: ['users'],
fetcher: (cursor) => api.getUsers(cursor: cursor),
getNextPageParam: (lastPage) => lastPage.nextCursor,
builder: (context, state) {
final items = state.pages!.expand((p) => p.items).toList();
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
if (index == items.length - 1 && state.hasMore == true) {
state.fetchMore();
}
return UserCard(items[index]);
},
);
},
)
Mutation #
HopeMutationBuilder<User, Map<String, dynamic>>(
mutation: (data) => api.updateUser(data),
onSuccess: (user) => HopeClient.instance.invalidate(['users']),
builder: (context, state, mutate) {
return ElevatedButton(
onPressed: state.isMutating ? null : () => mutate({'name': 'Hope'}),
child: state.isMutating ? CircularProgressIndicator() : Text('Save'),
);
},
)
Reset Mutation
HopeMutationBuilder<User, Map<String, dynamic>>(
mutation: (data) => api.updateUser(data),
builder: (context, state, mutate) {
if (state.isError) return ElevatedButton(
onPressed: state.reset,
child: Text('Try Again'),
);
return ElevatedButton(
onPressed: state.isMutating ? null : () => mutate({'name': 'Hope'}),
child: state.isMutating ? CircularProgressIndicator() : Text('Save'),
);
},
)
Invalidation #
// single query
HopeClient.instance.invalidate(['users']);
// all queries
HopeClient.instance.invalidateAll();
// by prefix
HopeClient.instance.invalidatePrefix(['user']);
Prefetch #
// warm cache before user navigates
HopeClient.instance.prefetch(
queryKey: ['user', userId],
fetcher: () => api.getUser(userId),
);
Optimistic Updates #
// update UI instantly before mutation completes
await HopeClient.instance.cache
.set('users', updatedUsers);
await mutation.mutate(input);
// get real server data
HopeClient.instance.invalidate(['users']);
License #
Apache 2.0 - see LICENSE file.
Author #
Hope Richard
📧 hoperichardmaleko@gmail.com
🔗 GitHub