Setting up the client
Configure one QueryClient for the whole app: set defaults, report every failure in one place, and catch the errors that callbacks throw. Do it once, in main, before anything creates an observer. Part of the app can run on a client of its own, for example in a widget test. The QueryClient reference lists every constructor option.
Creating the client
Section titled “Creating the client”Fuery.client is the client that widgets use without a FueryProvider. observe() and a definition’s mutate use it too, unless you pass a client. Fuery creates it on first use, so an app that configures nothing still works.
To configure it, assign a new client in main:
void main() { Fuery.client = QueryClient( defaultOptions: const DefaultOptions( queries: QueryDefaults(staleTime: Duration(seconds: 30)), ), ); runApp(const App());}- Assigning
Fuery.clientmounts the new client and unmounts the previous one. - A mounted client refetches on focus and on reconnect, and resumes paused mutations.
- Assign it before anything creates an observer. An observer keeps the client it was created with.
- Register defaults before observers exist too. An observer applies the defaults when it receives its options, not afterwards.
Setting defaults
Section titled “Setting defaults”Set defaults for every query and mutation of the client, or for a key prefix:
Fuery.client = QueryClient( defaultOptions: const DefaultOptions( queries: QueryDefaults(staleTime: Duration(seconds: 30)), mutations: MutationDefaults(retry: RetryPolicy.count(2)), ),);
Fuery.client.setQueryDefaults( ['settings'], const QueryDefaults(staleTime: infiniteDuration),);
Fuery.client.setMutationDefaults( ['todos'], const MutationDefaults(networkMode: NetworkMode.offlineFirst),);- Options set on the query or mutation win over per-key defaults.
- Per-key defaults win over the client’s
defaultOptions. - A mutation gets per-key defaults only when it has a
mutationKey. getQueryDefaults(['settings'])andgetMutationDefaults(['todos'])return the per-key defaults for a key, merged from every matching prefix. They don’t includedefaultOptions.
Defaults lists the fields of QueryDefaults and MutationDefaults.
Reporting every failure in one place
Section titled “Reporting every failure in one place”Give the caches a config to run a callback for every query and every mutation, for example to report failures to a crash or logging service:
Fuery.client = QueryClient( queryCache: QueryCache( config: QueryCacheConfig( onError: (error, query) => reportError(error, query.queryKey), ), ), mutationCache: MutationCache( config: MutationCacheConfig( onError: (error, variables, context, mutation) => reportError(error, mutation.options.mutationKey), ), ),);- A cache keeps its config for its whole life, so pass the config when you construct the client.
QueryCacheConfigcallbacks run after a fetch. A cancelled fetch isn’t a failure and reaches none of them.MutationCacheConfigcallbacks run before the callbacks of the mutation itself, and Fuery awaits a future they return.- The callbacks receive each mutation run as an
AnyCachedMutation, whosedata,variables, andcontextareObject?. Tell mutations apart bymutation.options.mutationKeyormutation.options.meta.
Cache callbacks lists every callback and when it runs.
Catching errors that callbacks throw
Section titled “Catching errors that callbacks throw”onUncaughtError receives the errors that no caller can catch, so you decide how to record them:
- An error thrown by a
QueryCacheConfigorMutateOptionscallback. - An error thrown by
onErrororonSettledafter a mutation failed, from the mutation or from itsMutationCacheConfig. - An error thrown by
refetchWhileorplaceholderDatawhile Fuery updates an observer after its query changed. - An error thrown by a listener: the
listenerof a listener widget, a consumer, or a hook, or a function passed to a slot’slistenorsubscribeToRuns. - A mistake Fuery finds while running, such as a
getNextPageParamthat returns a param of the wrong type or throws while a result is built, or a persistedmutationKeythat can’t be stored.
Fuery.client = QueryClient( onUncaughtError: (error, stackTrace) => reportError(error, stackTrace),);- The query or mutation goes on as if the callback hadn’t thrown.
- Fuery reports each mistake once per client, not every time the code runs.
- When
onUncaughtErrorthrows, its error and the error it received both go to the current zone.
Without onUncaughtError, these errors go to the current zone, and Flutter passes them to PlatformDispatcher.onError. A crash reporter that records everything there as fatal then counts them as crashes, although the app keeps running.
The other callbacks of a Mutation and a MutationCacheConfig are part of the mutation. An error thrown by onMutate, or by onSuccess or onSettled after a success, fails the mutation and reaches its onError.
Which client a query uses
Section titled “Which client a query uses”A Query holds no client, so one definition works with every client. Fuery picks the client where the query is used:
- A widget or hook that gets a definition uses the client of the nearest
FueryProvider, orFuery.clientwithout one. It follows a provider whose client is replaced. observe()uses the client you pass asclient:, orFuery.clientat that moment. The observer keeps that client for its whole life, andobserver.clientreturns it.- A definition’s
mutateandmutateAsyncuse the client you pass them, orFuery.clientat that moment. In a widget, passcontext.queryClient, so the run reaches the cache that the widgets read. - A widget or hook that gets an observer uses the observer’s client. In debug builds, it prints a warning when that isn’t its own client. See A screen reads another client’s cache.
- Query functions,
placeholderData, and mutation callbacks receive the client that runs them.
Queries can therefore be top-level values. Widget tests that each get a fresh client, through Fuery.client or a FueryProvider, need nothing else.
Giving a subtree its own client
Section titled “Giving a subtree its own client”Wrap part of the app in FueryProvider to run it on another client, for example in a widget test. Keep the client in a State field, so the subtree keeps one client while it is mounted:
class _SettingsPageState extends State<SettingsPage> { final client = QueryClient();
@override Widget build(BuildContext context) { return FueryProvider(client: client, child: const SettingsView()); }}- Create the client once: in
main, in aStatefield, or in a test’ssetUp. - Don’t create it in
build. AQueryClientcreated there is a new, empty cache on every rebuild and every hot reload, so the widgets below go back to loading and fetch again. FueryProvidermounts the client and unmounts it when the provider goes away, so theStateneeds nodispose.
Widgets below the provider use its client. context.queryClient returns it, or Fuery.client when there is no provider. Pass it to observe for an observer of your own:
late final todos = todosQuery.observe(client: context.queryClient);Pass it to a definition’s mutate too: addTodo.mutate('Buy milk', context.queryClient).
An adapter for another state library reads the client with FueryProvider.of(context, listen: true), which rebuilds when the provider’s client is replaced.
In the example app
Section titled “In the example app”The example assigns Fuery.client in main, with a storage, and restores stored mutations before runApp. Its README maps each screen to what it shows.