Skip to main content

Widgets and controllers

This page lists the Flutter binding's public surface: the provider, the four call styles for reading queries and mutations, the side-effect listeners, the collection helpers and the connectivity value. Everything here comes from one import, package:query_kit_flutter/query_kit_flutter.dart, which also re-exports the whole core. Where a member has a TanStack Query counterpart, its description names it ("TanStack: useQuery").

For the options these widgets take, see query options; for what they hand back, see results; for the client itself, see QueryClient. The guides explain when to reach for what: four ways to read a query, what rebuilds and mutations.

The four call styles at a glance​

The binding reads queries, infinite queries and mutations in four equal styles. None is the default, the order of the rows means nothing, and the styles mix freely inside one screen. Each takes the same options objects and each takes a buildWhen (a controller filters in its listener instead, because it is the notifier).

StyleQueryInfinite queryMutation
Builder widgetsQueryBuilder, QuerySelectBuilderInfiniteQueryBuilderMutationBuilder
ControllersQueryController, QueryController.createInfiniteQueryControllerMutationController
BuildContext extensioncontext.query, context.selectQuerycontext.infiniteQuerycontext.mutation
State mixinwatchQuery, watchSelectQuerywatchInfiniteQuerywatchMutation

What they share:

  • One observer per reader, one query per key. Every builder, controller and keyless read owns its own observer. The query in the cache is shared, so two readers of one key cost one request.
  • Options are re-applied whenever the reader is built with them: on every build for context.query and the mixin, and whenever the parent rebuilds a builder widget. An Enabled.when over outside state is re-evaluated then; the observer compares the defaulted options by value, and only a real difference reaches the query. A changed key switches the observed query in place for a builder, a controller's setOptions and a read with an id; a read without an id is identified by its key, so a new key is a new observer and the old one is released (see identity).
  • No notification for nothing. A reader is never rebuilt for a notification carrying what it is already showing. Under a QueryClientProvider, a result that changes during a build is delivered once the build is over.
  • Infinite queries hand out the controller. Paging lives on InfiniteQueryController, so the infinite-query form of each style gives you the controller rather than a bare result. A change of the paging flags alone rebuilds even when the result is equal.
  • Mutations hand out the controller. mutate lives on MutationController, so the mutation form of each style gives you the controller; its value is the MutationResult.

QueryClientProvider​

Dartdoc · TanStack Query: QueryClientProvider

A StatefulWidget that provides a QueryClient to the widgets below it and wires the client to Flutter while it is mounted. Put one above everything that reads a query.

Constructors​

ConstructorMeaning
QueryClientProvider.create({key, required create, required child, onlineStatus, observeAppLifecycle, isAppShown})A static method returning a Widget. Calls create once and owns the client: it lives as long as the provider and is cleared (client.clear()) after the provider unmounts. Rebuilding with a different create callback keeps the client; change key to replace it.
QueryClientProvider({key, required client, required child, onlineStatus, observeAppLifecycle, isAppShown})const. Takes a client you made yourself — one configured with defaultOptions, shared with code outside the tree, or created in a test — and never clears it. A different client on a later build unmounts the old one and mounts the new one.

Parameters​

ParameterTypeDefaultMeaning
create (.create only)QueryClient Function()requiredMakes the owned client, once, in initState. QueryClient.new is the shortest form. TanStack: new QueryClient().
client (unnamed only)QueryClientrequiredThe client every builder, controller and keyless read below runs on unless it names its own. Swapping it recreates every observer below, because each belonged to the old client. TanStack: the provider's client prop.
childWidgetrequiredThe subtree that can reach the client. TanStack: children.
onlineStatusOnlineStatus?nullConnectivity, if you bring it: OnlineStatus.fixed or OnlineStatus.stream. null installs nothing and the client assumes it is online. TanStack: onlineManager.setEventListener.
observeAppLifecyclebooltrueWhether to map the app's lifecycle onto the client's focus state. Turn it off when you install your own focus source with client.focusManager.setEventListener(…): both write through setFocused, so with both the last writer wins. TanStack: focusManager, whose default listens to the browser's visibilitychange.
isAppShownbool Function(AppLifecycleState state)?null (built-in mapping)Which lifecycle states count as focused. The mapping given on the latest build is in force: it is applied to the current state when it changes, and decides every transition after.
keyKey?nullFor .create, a new key is the way to get a new client.

The built-in focus mapping: resumed is focused; hidden, paused and detached are not. inactive counts as focused on iOS, Android and Fuchsia, where it is a transient interruption (the notification shade, an incoming call), and as unfocused on macOS, Windows and Linux, where it means the window lost focus. The state the app is already in at mount is mapped too, not only later transitions. See app focus refetching.

At mount the provider also installs a notify scheduler on the client: a notification arriving during a frame's build, layout or paint is deferred to a post-frame callback, and — in debug builds — one arriving during a build outside a frame (the root's first build in runApp) to a microtask, so a query resolving mid-build cannot call setState during that build. (The assertion that guards against that is debug-only; in release such a notification runs at once.) Several providers may share one client; the scheduler stays until the last of them goes.

Static members​

MemberTypeMeaning
of(context)QueryClientThe nearest client above context, subscribing the caller to a change of client. For build. Throws a FlutterError when there is no provider. TanStack: useQueryClient().
maybeOf(context)QueryClient?The same, null without a provider. Subscribes like of.
read(context)QueryClientLike of, without subscribing. For callbacks, initState and anywhere outside build. Throws a FlutterError when there is no provider.

Builder widgets​

The StreamBuilder shape. Each widget creates its controller on its first build (nothing is fetched before that), owns it until it is disposed, and rebuilds its own subtree. Do not dispose a controller a builder hands you.

All four take client: null uses the nearest QueryClientProvider's. What counts is the client resolved, so naming the provider's own client changes nothing; a different resolved client on a later build — this field's or the provider's — recreates the controller. Changed options on the same client are applied in place.

QueryBuilder​

Dartdoc · TanStack Query: useQuery

QueryBuilder<TData> — a query without select. The type argument comes from queryFn's return type or is written out; an options literal with neither is refused by an assertion in debug builds.

ParameterTypeDefaultMeaning
optionsQueryObserverOptions<TData>requiredThe query. Re-applied whenever the parent rebuilds this widget.
builderWidget Function(BuildContext context, QueryResult<TData> result)requiredBuilds the subtree from the sealed QueryResult. Called on the first build, then for each changed result buildWhen lets through.
buildWhenBuildWhen<QueryResult<TData>>?null (every change)Whether a change from the result last built to the current one rebuilds. TanStack: notifyOnChangeProps, a different mechanism.
clientQueryClient?null (provider's)The client to observe on. TanStack: the hook's queryClient argument.
keyKey?nullThe widget's key.

QuerySelectBuilder​

Dartdoc · TanStack Query: useQuery with select

QuerySelectBuilder<TQueryData, TData> — the cache holds TQueryData, the builder sees TData. The parameters are those of QueryBuilder, with these types:

ParameterTypeDefaultMeaning
optionsQuerySelectOptions<TQueryData, TData>requiredThe query and its required select, which lets Dart infer TData.
builderWidget Function(BuildContext context, QueryResult<TData> result)requiredBuilt from the selected result.
buildWhenBuildWhen<QueryResult<TData>>?nullCompares selected results.
clientQueryClient?nullAs for QueryBuilder.
keyKey?nullThe widget's key.

A select that returns an equal value keeps the previous instance, but the rest of the result (fetchStatus, dataUpdatedAt) still changes; that is what buildWhen is for.

InfiniteQueryBuilder​

Dartdoc · TanStack Query: useInfiniteQuery

InfiniteQueryBuilder<TPageData, TPageParam, TData> — no type arguments at the call site; inference reads all three off the options.

ParameterTypeDefaultMeaning
optionsInfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData> (InfiniteQueryObserverOptions or InfiniteQuerySelectOptions)requiredKey, page function and paging functions. Re-applied whenever the parent rebuilds this widget.
builderWidget Function(BuildContext context, InfiniteQueryController<TPageData, TPageParam, TData> query)requiredGiven the controller, not a bare result: the pages are in query.value, paging is query.fetchNextPage and its siblings.
buildWhenBuildWhen<QueryResult<TData>>?nullCompares the controller's results. A change of the paging flags alone rebuilds regardless.
clientQueryClient?nullAs for QueryBuilder. TanStack: the hook's queryClient argument.
keyKey?nullThe widget's key.

MutationBuilder​

Dartdoc · TanStack Query: useMutation

MutationBuilder<TData, TVariables, TOnMutateResult> — the type arguments come from the options; MutationOptions.simple infers them from mutationFn.

ParameterTypeDefaultMeaning
optionsMutationOptions<TData, TVariables, TOnMutateResult>requiredThe mutation function and callbacks. Re-applied whenever the parent rebuilds this widget, so the next run uses the latest ones; a run still in flight takes the new callbacks, meta and gcTime, and a retry calls the new mutation function, but it keeps the retry, retryDelay, networkMode and scope it started with. A different mutationKey (both set) resets the reader to idle.
builderWidget Function(BuildContext context, MutationController<TData, TVariables, TOnMutateResult> mutation)requiredGiven the controller: the result is mutation.value, and mutate or mutateAsync starts a run.
buildWhenBuildWhen<MutationResult<TData, TVariables>>?nullThe only filter a mutation reader has; it has no select.
clientQueryClient?nullThe client to run on. TanStack: the hook's queryClient argument.
keyKey?nullThe widget's key.

Disposing the widget does not cancel a run in flight: the mutation finishes and its options' callbacks run. Call MutationController.cancel first when it should not.

Controllers​

Plain ValueListenables, usable without widgets: in view models, with ValueListenableBuilder or ListenableBuilder, or in any state-management package that reads a listenable. You create one, listen to it and dispose it. Do not create one in build: every rebuild would leak an observer and its timers.

Every controller here is subscribed only while listened to: the first listener subscribes it to its observer (which is when a query fetches, if it needs to), the last one leaving unsubscribes it. It notifies only when something a reader can see changed, and under a QueryClientProvider never in the middle of a build.

QueryController​

Dartdoc · TanStack Query: useQuery (over a QueryObserver)

QueryController<TQueryData, TData> extends ChangeNotifier and implements ValueListenable<QueryResult<TData>>.

MemberTypeDefaultMeaning
QueryController(client, options)constructor; options is QueryObserverOptionsBase<TQueryData, TData>—Creates the observer. Takes either options shape, a QuerySelectOptions included. Nothing is fetched until the first listener. Asserts in debug builds that TData is not a top type. TanStack: new QueryObserver(client, options).
QueryController.create(client, options)static, returns QueryController<TData, TData>; options is QueryObserverOptions<TData>—The form without select, with one type argument.
QueryController.observing(client, observer)constructor; observer is QueryObserver<TQueryData, TData>—Wraps an observer built elsewhere and owns it from then on. Holds no options of its own, so value before the first listener is the observer's current result. The same top-type assertion as the unnamed constructor.
clientQueryClient—The client the observer runs on.
valueQueryResult<TData>—The current result. While nobody listens it is the optimistic result — fetching for a query that will fetch on subscribe — which is what every widget style shows on its first build.
setOptions(options)void; QueryObserverOptionsBase<TQueryData, TData>—Replaces the options in place; a new key switches the observed query. Does not notify by itself: a listener hears of the change only when the observer's result changes, as for any other notification. When the observer refuses the options, nothing is kept. TanStack: observer.setOptions.
refetch({cancelRefetch})Future<QueryResult<TData>>cancelRefetch: trueRefetches. true cancels a fetch in flight and starts again; false joins it. TanStack: refetch.
observerQueryObserver<TQueryData, TData>—The observer underneath, for what the controller does not mirror, such as currentQuery.
isDisposedbool—Whether dispose has run.
observedStateObject?—What a notification is compared on before a reader is told: value for a plain query; for an infinite one, value together with its six paging flags. The binding's readers use it; application code rarely needs it.
optimisticValueQueryResult<TData> (@protected)—The result the observer would report on subscribing now; what value reports while nobody listens. For subclasses.
addListener / removeListenervoid—The first listener subscribes, the last one leaving unsubscribes.
dispose()void—Destroys the observer. Runs once.

InfiniteQueryController​

Dartdoc · TanStack Query: useInfiniteQuery (over an InfiniteQueryObserver)

InfiniteQueryController<TPageData, TPageParam, TData> extends QueryController<InfiniteData<TPageData, TPageParam>, TData>, so client, value, refetch, observer, isDisposed and dispose are inherited. Its notifications also cover the paging flags: two fetches in opposite directions leave the result equal and still notify.

MemberTypeDefaultMeaning
InfiniteQueryController(client, options)constructor; options is InfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData>—Creates an InfiniteQueryObserver. Same contract as QueryController. TanStack: new InfiniteQueryObserver(client, options).
setInfiniteOptions(options)void; InfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData>—Replaces the options, paging half included. Does not notify by itself, as for QueryController.setOptions. TanStack: observer.setOptions.
setOptions(options)void; QueryObserverOptionsBase<InfiniteData<TPageData, TPageParam>, TData>—Accepts only options that carry the paging behaviour; plain observer options throw an UnsupportedError in every build mode. TanStack: observer.setOptions.
hasNextPagebool—getNextPageParam returns a param for the pages held. False before the first page. TanStack: hasNextPage.
hasPreviousPagebool—getPreviousPageParam returns a param. Always false without one. TanStack: hasPreviousPage.
isFetchingNextPagebool—The fetch in flight is a fetchNextPage. TanStack: isFetchingNextPage.
isFetchingPreviousPagebool—The fetch in flight is a fetchPreviousPage. TanStack: isFetchingPreviousPage.
isFetchNextPageErrorbool—The result's error came from a fetchNextPage; the pages held are still there. TanStack: isFetchNextPageError.
isFetchPreviousPageErrorbool—The error came from a fetchPreviousPage. TanStack: isFetchPreviousPageError.
isRefetchingbool—The pages held are being refetched, and no fetchNextPage or fetchPreviousPage is in flight. TanStack: isRefetching.
isRefetchErrorbool—A refetch of the held pages failed, as opposed to a page fetch. TanStack: isRefetchError.
fetchNextPage({cancelRefetch})Future<QueryResult<TData>>cancelRefetch: trueFetches the page after the ones held. TanStack: fetchNextPage.
fetchPreviousPage({cancelRefetch})Future<QueryResult<TData>>cancelRefetch: trueFetches the page before the ones held. TanStack: fetchPreviousPage.
infiniteObserverInfiniteQueryObserver<TPageData, TPageParam, TData>—observer, typed with the paging half visible.

MutationController​

Dartdoc · TanStack Query: useMutation (over a MutationObserver)

MutationController<TData, TVariables, TOnMutateResult> extends ChangeNotifier and implements ValueListenable<MutationResult<TData, TVariables>>.

MemberTypeDefaultMeaning
MutationController(client, options)constructor; options is MutationOptions<TData, TVariables, TOnMutateResult>—Creates a MutationObserver. Idle until a run starts. TanStack: new MutationObserver(client, options).
clientQueryClient—The client the observer runs on.
valueMutationResult<TData, TVariables>—The current result.
mutate(variables, {callbacks})void; callbacks is MutateCallbacks<TData, TVariables, TOnMutateResult>?callbacks: nullFire and forget: the result lands in value, errors never reach the caller. TanStack: mutate.
mutateAsync(variables, {callbacks})Future<TData>callbacks: nullCompletes with the data or throws. The per-call callbacks run after the options' own for as long as the controller is not disposed, listened to or not. Called after dispose, the mutation still runs with its options' callbacks, nothing lands in value, and the per-call callbacks are dropped. TanStack: mutateAsync.
setOptions(options)void; MutationOptions<TData, TVariables, TOnMutateResult>—Replaces the options the next run uses; a run still in flight takes the new callbacks, meta and gcTime, and a retry calls the new mutation function, but it keeps the retry, retryDelay, networkMode and scope it started with. Does not notify by itself — except that a different mutationKey (both set) resets the controller to idle, which listeners hear like any other change. TanStack: observer.setOptions.
reset()void—Back to idle, detaching from the mutation being observed. TanStack: reset.
cancel()void—Fails the run this controller shows with a CancelledError; its error callbacks run. With nothing running it does nothing, and earlier runs are not touched. See cancelling mutations.
observerMutationObserver<TData, TVariables, TOnMutateResult>—The observer underneath, for its defaulted options, say. Run mutations through the controller: observer.mutate on a controller nobody listens to drops the per-call callbacks.
isDisposedbool—Whether dispose has run.
addListener / removeListenervoid—The first listener subscribes the controller to its observer, the last one leaving unsubscribes it. A run started by mutate or mutateAsync holds its own subscription until it settles, so its per-call callbacks run on a controller nobody listens to.
dispose()void—Detaches the observer from whatever mutation it ran, so that mutation can be collected. Does not cancel a run in flight.

context.query and its siblings​

Dartdoc · TanStack Query: useQuery, useInfiniteQuery, useMutation

QueryContext is an extension on BuildContext. It works in a StatelessWidget, needs no wrapper widget, and rebuilds per reader: only the widgets that read a query rebuild when it changes. There is no client: parameter: it always reads the nearest QueryClientProvider's client, and without a provider every member throws a FlutterError.

MemberReturnsParametersMeaning
query<TData>(options, {id, buildWhen})QueryResult<TData>QueryObserverOptions<TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenThe query's current result; this widget rebuilds when it changes. TanStack: useQuery.
selectQuery<TQueryData, TData>(options, {id, buildWhen})QueryResult<TData>QuerySelectOptions<TQueryData, TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenquery with a select: the cache holds TQueryData, this widget sees TData. TanStack: useQuery with select.
infiniteQuery<TPageData, TPageParam, TData>(options, {id, buildWhen})InfiniteQueryController<TPageData, TPageParam, TData>InfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenThe infinite query's controller, owned by this widget. TanStack: useInfiniteQuery.
mutation<TData, TVariables, TOnMutateResult>(options, {id, buildWhen})MutationController<TData, TVariables, TOnMutateResult>MutationOptions<TData, TVariables, TOnMutateResult> options, Object? id, BuildWhen<MutationResult<TData, TVariables>>? buildWhenA mutation's controller, owned by this widget. TanStack: useMutation.

The controllers infiniteQuery and mutation return belong to the reading widget and are disposed for you; do not dispose them. id and buildWhen default to null.

Identity​

  • A query read is identified by its key and its types, not by call order, so a read inside an if is fine. Two readers of one key share the query in the cache but not an observer.
  • id takes the key's place in the read's identity (the types stay part of it). A read with an id keeps its observer when its key changes, which is what keeping the previous key's data on screen with a placeholder needs. id also tells apart two reads of one key with different selectors of the same output type; two such reads without one are caught in debug builds.
  • A mutation read is identified by id, else by its mutationKey, each with its three types, else by the types alone. Two reads of one identity in one build share one controller, so without an id a debug assertion fires when they differ in the mutation function (mutationFn or mutationFnWithContext), in onMutate, onSuccess, onError or onSettled, or in scope, retry, retryDelay, networkMode or gcTime. Those five compare by value, except RetryPolicy.when and RetryDelay.dynamic, which compare by variant only. meta is not compared. Only reads in a StatelessWidget's or State's own build are compared; a nested builder re-reading through the outer context, and a read through a LayoutBuilder's context, are not. A function literal is a new function on every build, so keep it in a field or read the mutation once.

Which element a read belongs to, and when it is released​

A read belongs to the element whose context it went through, rebuilds that element, and lives as long as that element's own builds keep reading it.

SituationWhat happens
A key read in the last build but not in this oneReleased after the frame. A mutation too.
The widget unmountsEverything it read is released. Releasing a mutation's controller does not cancel a run in flight.
A widget stops reading altogetherNo signal Flutter can see: its last observers stay until it unmounts. Put a conditional read in a small widget of its own.
A read outside build (a tap handler)Creates an observer that is only matched up at the next build. Read in build, act in the handler.
A read through the outer context inside a ValueListenableBuilder, AnimatedBuilder or LayoutBuilder callbackAdditive: it releases nothing the widget's own build read. A key the callback stops reading stays until the widget's next own build that reads, or its unmount. A build that reads nothing itself never starts over; those keys stay until the parent rebuilds the widget or it unmounts.
A read through the context a LayoutBuilder, SliverLayoutBuilder or OrientationBuilder hands its builderStarts over whenever that builder provably runs: its constraints changed, its parent rebuilt it, or one of its own reads notified. A rebuild caused only by an InheritedWidget it depends on carries no signal, so a key picked from an inherited value stays until the next of those.
A read from a showDialog or bottom-sheet builder through the page's contextRebuilds the page, not the dialog, and the page's next build releases it while the dialog may still show it. Read through the builder's own context, or in a widget inside the dialog.
A read through the context a ListView.builder, GridView.builder, PageView.builder or another lazily built list hands its item builderDebug builds throw a FlutterError naming the fix. That context belongs to the whole list, not the row. In release builds the reads are additive: no row on screen loses its subscription, and rows scrolled away stay subscribed until the list is rebuilt or unmounts. Give each row a widget of its own and read in its build.
The provider's client changesEvery observer read through it is released, and the readers rebuild and recreate what they read on the new client.

QueryMixin​

Dartdoc · TanStack Query: useQuery, useInfiniteQuery, useMutation

mixin QueryMixin<T extends StatefulWidget> on State<T>. Reads like a hook, flat in a State's build, and everything it creates belongs to the State: disposed with it, and recreated when its client changes.

MemberReturnsParametersMeaning
queryClientQueryClient (getter)—The client every read runs on; QueryClientProvider.of(context) by default. Override it to read from another client, widget.client say. Looked up again at every read: a different client releases everything held and recreates it on the new one. A build that reads nothing needs no provider. TanStack: useQueryClient().
watchQuery<TData>(options, {id, buildWhen})QueryResult<TData>QueryObserverOptions<TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenSubscribes this State to the query and returns its current result. TanStack: useQuery.
watchSelectQuery<TQueryData, TData>(options, {id, buildWhen})QueryResult<TData>QuerySelectOptions<TQueryData, TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenwatchQuery with a select. TanStack: useQuery with select.
watchInfiniteQuery<TPageData, TPageParam, TData>(options, {id, buildWhen})InfiniteQueryController<TPageData, TPageParam, TData>InfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhenThe controller belongs to the State; do not dispose it. TanStack: useInfiniteQuery.
watchMutation<TData, TVariables, TOnMutateResult>(options, {id, buildWhen})MutationController<TData, TVariables, TOnMutateResult>MutationOptions<TData, TVariables, TOnMutateResult> options, Object? id, BuildWhen<MutationResult<TData, TVariables>>? buildWhenA mutation owned by this State, not shared. TanStack: useMutation.

The mixin also overrides dispose, releasing everything the State read.

Identity is the same as for context.query: key and types, or id; for a mutation id, else mutationKey, with the same debug assertion. Release differs in one way, because a State is one reader:

  • A key read in the previous build but not in this one is released after the frame; everything goes when the State is disposed. A State that stops reading altogether keeps its last observers until it is disposed.
  • A watchQuery inside a nested builder callback — a ValueListenableBuilder, LayoutBuilder, AnimatedBuilder, or a ListView.builder's itemBuilder — reads for this State and is additive. It is not refused in debug builds. A key the callback stops reading stays until an own build of this State that reads, or its disposal. For a long list, give each row a widget of its own.
  • A read always rebuilds this State. A dialog builder calling watchQuery is not rebuilt by a change; give a dialog a reader of its own.

BuildWhen and ListenWhen​

BuildWhen​

Dartdoc · TanStack Query: notifyOnChangeProps (a different mechanism)

typedef BuildWhen<T> = bool Function(T previous, T current)

Whether a reader rebuilds for a change from previous to current. Every builder widget, context read and mixin read takes one.

  • It is asked only when the value really changed. A notification carrying what the reader already shows never rebuilds, with or without a predicate.
  • previous is what is on screen: the result the reader last built from. When the predicate returns false, that stays previous, and the next change is compared against it.
  • select narrows the data a reader sees; buildWhen narrows when it rebuilds. It is the tool for a change select cannot see: a background refetch moves fetchStatus and dataUpdatedAt, both part of a result's ==.
  • Each keyless read has its own predicate, and any one of them letting a change through rebuilds the whole widget or State.
  • For infinite queries it compares results; a change of the paging flags alone rebuilds regardless.
  • Controllers, QueriesBuilder and QueriesController take none. A controller is the notifier, and a predicate on it would impose one listener's filter on every listener. See buildWhen.

ListenWhen​

Dartdoc

typedef ListenWhen<T> = bool Function(T previous, T next)

Whether a transition runs a listener widget's side effect. It sees every transition, and a rejected one still becomes the previous of the next.

Listeners​

QueryListener, InfiniteQueryListener, MutationListener · TanStack Query: — (a useEffect over the result)

Run a side effect — a snackbar, a navigation, a log line — when a controller's result changes, without rebuilding child. Each listens to a controller you own and never disposes it. Its listening counts like any other: a controller nobody listened to is subscribed when the listener mounts, and a query fetches then if it needs to. See side effects.

ParameterTypeDefaultMeaning
controllerQueryController<TQueryData, TData> / InfiniteQueryController<TPageData, TPageParam, TData> / MutationController<TData, TVariables, TOnMutateResult>requiredThe controller to listen to. Borrowed: whoever created it disposes it. A different controller on a later build is listened to from then on, and transitions of the old one still queued are dropped.
listenervoid Function(BuildContext context, T result)requiredThe side effect. T is QueryResult<TData> for the two query listeners and MutationResult<TData, TVariables> for MutationListener.
listenWhenListenWhen<T>?null (every change)Which transitions run listener.
childWidgetrequiredReturned unchanged; a transition never rebuilds it.
keyKey?nullThe widget's key.

When listener runs:

  • Not on mount. Only a later change of the controller's value is a transition.
  • Outside the build phase, in a microtask after the notification, so it may show a dialog, navigate or call setState. Each transition is delivered with the value it carried, even if a later one has arrived by then. listenWhen is asked in the same microtask.
  • Once per notification. Two cache writes inside one notifyManager.batch are one transition, to the second value.
  • For InfiniteQueryListener, only on a change of the result. A paging flag changing on its own is not a transition.
  • An error thrown by listener is reported through FlutterError, not thrown into the tree.

For a side effect of one particular call, the per-call callbacks of MutationController.mutate are the alternative; MutationListener hears every run of the controller.

Collections​

QueriesBuilder​

Dartdoc · TanStack Query: useQueries

QueriesBuilder<TQueryData, TData> builds from a list of queries of one type that may change length or order. It owns a QueriesController.

ParameterTypeDefaultMeaning
queriesList<QueryObserverOptionsBase<TQueryData, TData>>requiredThe queries, in the order their results are handed to builder. Re-applied whenever the parent rebuilds this widget; an unchanged list moves nothing. TanStack: queries.
builderWidget Function(BuildContext context, List<QueryResult<TData>> results)requiredBuilt from one result per entry of queries, first and again whenever a result or the list changes.
clientQueryClient?null (provider's)As for QueryBuilder. TanStack: the hook's queryClient argument.
keyKey?nullThe widget's key.
  • Observers are reused by key and occurrence, so reordering starts no requests. Duplicate keys share one cache entry and keep their own options.
  • Each query fails and settles on its own.
  • No buildWhen. A predicate over the whole list would say nothing about which query changed; read each query on its own to filter per query. The widget still never rebuilds for results it already shows, compared element by element.
  • For queries of different types, read each one and combine them; see combining queries.

QueriesController​

Dartdoc · TanStack Query: useQueries (over a QueriesObserver)

QueriesController<TQueryData, TData> is a ValueListenable<List<QueryResult<TData>>>, the same collection outside a widget.

MemberTypeDefaultMeaning
QueriesController(client, queries)constructor; List<QueryObserverOptionsBase<TQueryData, TData>> queries—Creates the collection. Nothing fetches until the first listener; then every enabled query that needs to does. TanStack: new QueriesObserver(client, queries).
clientQueryClient—The client every query in the collection uses.
valueList<QueryResult<TData>>—The results, in order. Before the first listener, the optimistic list.
setQueries(queries)void—Replaces the list, reusing observers by key occurrence. A new key fetches like any new query. TanStack: observer.setQueries.
observerQueriesObserver<TQueryData, TData>—The core observer, including its underlying observers.
addListener / removeListenervoid—The first listener subscribes the collection, the last one leaving unsubscribes it.
dispose()void—Destroys every observer.

Listeners are told only when a result changed, compared element by element; each result's refetch target is compared too, so replacing or reordering equal results still notifies.

IsFetchingController​

Dartdoc · TanStack Query: useIsFetching

A ValueListenable<int>: how many queries matching the filters are fetching right now. A background refetch counts. Subscribed to the query cache only while something listens, and notifies only when the count changes.

MemberTypeDefaultMeaning
IsFetchingController(client, {filters})constructorfilters: const QueryFilters() (all queries)Creates the count over client's query cache. TanStack: useIsFetching(filters).
clientQueryClient—The client whose queries are counted.
filtersQueryFiltersall queriesWhich queries count. Fixed for the controller's life; other filters are another controller. TanStack: filters.
valueint—client.isFetching(filters: filters).
addListener / removeListenervoid—The first listener subscribes to the query cache, the last one leaving unsubscribes.
dispose()void—Unsubscribes.

QueryClient.isFetching is the same count as a one-off snapshot. For mutations in flight, use a MutationStateController filtered on MutationStatus.pending; its list length is the count.

MutationStateController​

Dartdoc · TanStack Query: useMutationState

MutationStateController<TSelected> is a ValueListenable<List<TSelected>>: a value selected from every mutation in the cache that matches the filters, one entry per mutation, in the order they were added. For showing mutations somewhere other than where they started — a "saving" badge, a pending row for a write started on another screen. See mutation state.

MemberTypeDefaultMeaning
MutationStateController(client, {filters, required select})constructor; select is MutationStateSelect<TSelected>, a TSelected Function(Mutation<Object?, Object?, Object?> mutation)filters: const MutationFilters()A selection over the mutation cache. select runs over every matching mutation on every cache change, so keep it cheap. TanStack: useMutationState({ filters, select }).
MutationStateController.typed(client, {filters, required select})static; select is TypedMutationStateSelect<TData, TVariables, TOnMutateResult, TSelected>, a TSelected Function(Mutation<TData, TVariables, TOnMutateResult> mutation)filters: const MutationFilters()Selects from mutations of one type only, typed. The types usually come from select's parameter; a type left as Object? matches anything. The type test stays through a later setOptions.
clientQueryClient—The client whose mutations are selected.
valueList<TSelected>—The current selection, as an unmodifiable list. Read while nobody listens, it is recomputed from the cache.
setOptions({filters, select})void; MutationFilters?, MutationStateSelect<TSelected>?both null (unchanged)Replaces the filters and/or selector. The selection is recomputed at once, and listeners are told when it changed.
addListener / removeListenervoid—The first listener subscribes to the mutation cache, the last one leaving unsubscribes.
dispose()void—Destroys the observer.

Subscribed to the mutation cache only while something listens; listeners are told only when the selection changed, element by element.

OnlineStatus​

Dartdoc · TanStack Query: onlineManager.setEventListener

A sealed value passed as QueryClientProvider.onlineStatus: what the client should believe about the network, and where later changes come from. Connectivity is opt-in — nothing is installed by default, no connectivity package is a dependency, and a client with no status assumes it is online. See connectivity for a connectivity_plus example.

VariantClassFieldsMeaning
OnlineStatus.fixed(bool online)OnlineStatusFixedonlineThis is the state, with no source of changes. For a test, a desktop build or a developer's offline switch.
OnlineStatus.stream(Stream<bool> changes, {required bool initial})OnlineStatusStreamchanges, initialFollow changes, assuming initial until the first event. initial is required because a Stream has no current value.

Both are const and compare by value. The base class exposes initial (bool: the whole story for fixed, the starting assumption for stream) and changes (Stream<bool>?, null for fixed). The classes are public so a switch can name them.

How the provider feeds it to client.onlineManager (through setOnline):

  • initial is applied whenever a client is given this status: at mount, to a client that arrives on a later build, and on any later build that changes the status — unless the old and the new status are both OnlineStatus.stream: a changed initial on the same stream, like a different stream, keeps the client's current verdict. Not when a stream delivers while it is being listened to (a synchronous controller's onListen): that event is believed over initial.
  • Every stream event is passed to the provider's current client.
  • A new client under the same stream starts from that stream's last event, or from initial if it has said nothing yet.
  • One stream swapped for another keeps the client's current verdict rather than rewinding it to initial, so a stream rebuilt in build does not flicker. A fixed status has no stream, so a changed one reaches the client on the rebuild that changes it and works as a live switch.
  • Taking the status away (null on a later build) or disposing the provider puts the client back online, but only once no other provider has a status for that client; a replacement provider on the same client keeps its own verdict. A client the provider leaves for another client is left as it was.
  • A stream is listened to once per stream object. A broadcast stream always works. A single-subscription stream works only while exactly one provider listens to it once; a second listen throws a FlutterError pointing to asBroadcastStream(). A stream error is reported through FlutterError.reportError, not thrown.

While the client believes it is offline, what a query or mutation does is its network mode.

Testing​

Widget tests need a teardown step, because a QueryClient outlives the tree and owns gcTime timers. It is a documented snippet rather than an export; see testing.