queryx_riverpod 0.1.1
queryx_riverpod: ^0.1.1 copied to clipboard
Riverpod bindings for QueryX — reactive queries, mutations, and infinite pagination with automatic widget rebuilds.
Riverpod bindings for QueryX with reactive providers and zero manual listener wiring.
Query, mutation, and infinite-query bindings for Riverpod — automatic rebuilds, shared caching, deduplication, retry, and invalidation through QueryX.
queryx_riverpod #
Riverpod bindings for queryx — Query, Mutation, and
InfiniteQuery exposed as StateNotifierProviders, so widgets get
automatic rebuilds with zero manual listener wiring.
This package is a thin adapter, not a second engine: every query still goes
through one shared QueryClient, so deduplication, caching, retry, and
invalidation all work exactly like they do in plain queryx — Riverpod just
gets to watch() the result.
Setup #
void main() {
final queryClient = QueryClient(
defaultStaleTime: const Duration(minutes: 5),
);
runApp(
ProviderScope(
overrides: [queryxClientProvider.overrideWithValue(queryClient)],
child: const MyApp(),
),
);
}
queryxClientProvider throws if you forget to override it — this is
intentional: constructing a throwaway client per-provider would silently
break cross-widget cache sharing, the entire point of queryx.
Queries #
final usersProvider = queryProvider<List<User>>(
QueryKey(['users']),
(ref) => () => ref.read(apiProvider).getUsers(),
);
class UsersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final users = ref.watch(usersProvider);
return users.when(
loading: () => const CircularProgressIndicator(),
error: (e) => Text(e.message),
data: (data) => ListView(children: [for (final u in data) Text(u.name)]),
);
}
}
queryProvider is .autoDispose: when the last widget watching it unmounts,
the underlying Query observer is released. If another widget elsewhere
still has the same key open, its data is untouched; otherwise queryx's own
cacheTime GC takes it from there. Riverpod's lifecycle and queryx's cache
lifecycle compose for free.
Dependent / parameterized queries #
queryProvider covers the common case (one query, no runtime argument). For
a query that depends on an argument — e.g. ['user', userId] — use
Riverpod's own .family directly with QueryNotifier:
final userProvider = StateNotifierProvider.autoDispose
.family<QueryNotifier<User>, QueryxSnapshot<User>, int>((ref, userId) {
final client = ref.watch(queryxClientProvider);
final query = client.query<User>(
QueryKey(['user', userId]),
() => ref.read(apiProvider).getUser(userId),
);
return QueryNotifier<User>(query);
});
// usage: ref.watch(userProvider(42))
For a query that should only run once another query has data (spec's
"dependent queries"), pass enabled based on that other provider's state:
final ordersProvider = queryProvider<List<Order>>(
QueryKey(['orders', userId]),
(ref) => () => api.getOrders(userId),
options: QueryOptions(enabled: ref.watch(userProvider(userId)).hasData),
);
Mutations #
final likePostProvider = mutationProvider<int, void, int>(
(ref) => (_) => api.post('/posts/1/like'),
optionsBuilder: (ref) {
final client = ref.read(queryxClientProvider);
return MutationOptions(
onMutate: (_) async {
final snapshot = client.getQueryData<int>(likesKey)!;
client.updateQueryData<int>(likesKey, (n) => (n ?? 0) + 1); // instant UI update
return snapshot;
},
onSuccess: (serverCount, _, __) => client.setQueryData(likesKey, serverCount),
onError: (error, _, snapshot) => client.setQueryData(likesKey, snapshot!), // rollback
);
},
);
ElevatedButton(
onPressed: () => ref.read(likePostProvider.notifier).mutate(null),
child: const Icon(Icons.favorite),
)
optionsBuilder (not a plain options value) is what it is because
onMutate/onSuccess/onError almost always need ref — usually to reach
queryxClientProvider for cache writes or client.invalidateQueries(...).
Infinite queries #
final feedProvider = infiniteQueryProvider<Post, int>(
QueryKey(['posts']),
initialPageParam: 0,
fetchPageBuilder: (ref) => (page) => ref.read(apiProvider).getPosts(page: page ?? 0),
);
final feed = ref.watch(feedProvider);
// feed.items, feed.hasNextPage, feed.isFetchingNextPage
ref.read(feedProvider.notifier).fetchNextPage();
Why this depends on riverpod, not flutter_riverpod #
The binding logic here (QueryNotifier, MutationNotifier,
InfiniteQueryNotifier) only needs StateNotifier and Ref — both live in
the pure-Dart riverpod package. That keeps this package unit-testable with
plain dart test (see test/), with no Flutter test harness required.
Inside a real Flutter app, flutter_riverpod's WidgetRef/ConsumerWidget
are built directly on top of riverpod's Ref/ProviderContainer, so
nothing here needs to change to be consumed from widgets — see
example/lib/main.dart for a full Flutter app using it.
Installation #
Add queryx_riverpod and flutter_riverpod to your Flutter app:
flutter_riverpod is only needed for Flutter UI integration, such as ConsumerWidget, ConsumerStatefulWidget, WidgetRef, and ProviderScope.
dependencies:
queryx_riverpod: ^0.1.0
flutter_riverpod: ^2.6.1 # for ConsumerWidget/WidgetRef in your app