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).
| Style | Query | Infinite query | Mutation |
|---|---|---|---|
| Builder widgets | QueryBuilder, QuerySelectBuilder | InfiniteQueryBuilder | MutationBuilder |
| Controllers | QueryController, QueryController.create | InfiniteQueryController | MutationController |
BuildContext extension | context.query, context.selectQuery | context.infiniteQuery | context.mutation |
State mixin | watchQuery, watchSelectQuery | watchInfiniteQuery | watchMutation |
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.queryand the mixin, and whenever the parent rebuilds a builder widget. AnEnabled.whenover 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'ssetOptionsand a read with anid; a read without anidis 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.
mutatelives onMutationController, so the mutation form of each style gives you the controller; itsvalueis theMutationResult.
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
| Constructor | Meaning |
|---|---|
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
| Parameter | Type | Default | Meaning |
|---|---|---|---|
create (.create only) | QueryClient Function() | required | Makes the owned client, once, in initState. QueryClient.new is the shortest form. TanStack: new QueryClient(). |
client (unnamed only) | QueryClient | required | The 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. |
child | Widget | required | The subtree that can reach the client. TanStack: children. |
onlineStatus | OnlineStatus? | null | Connectivity, if you bring it: OnlineStatus.fixed or OnlineStatus.stream. null installs nothing and the client assumes it is online. TanStack: onlineManager.setEventListener. |
observeAppLifecycle | bool | true | Whether 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. |
isAppShown | bool 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. |
key | Key? | null | For .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
| Member | Type | Meaning |
|---|---|---|
of(context) | QueryClient | The 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) | QueryClient | Like 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.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
options | QueryObserverOptions<TData> | required | The query. Re-applied whenever the parent rebuilds this widget. |
builder | Widget Function(BuildContext context, QueryResult<TData> result) | required | Builds the subtree from the sealed QueryResult. Called on the first build, then for each changed result buildWhen lets through. |
buildWhen | BuildWhen<QueryResult<TData>>? | null (every change) | Whether a change from the result last built to the current one rebuilds. TanStack: notifyOnChangeProps, a different mechanism. |
client | QueryClient? | null (provider's) | The client to observe on. TanStack: the hook's queryClient argument. |
key | Key? | null | The 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:
| Parameter | Type | Default | Meaning |
|---|---|---|---|
options | QuerySelectOptions<TQueryData, TData> | required | The query and its required select, which lets Dart infer TData. |
builder | Widget Function(BuildContext context, QueryResult<TData> result) | required | Built from the selected result. |
buildWhen | BuildWhen<QueryResult<TData>>? | null | Compares selected results. |
client | QueryClient? | null | As for QueryBuilder. |
key | Key? | null | The 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.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
options | InfiniteQueryObserverOptionsBase<TPageData, TPageParam, TData> (InfiniteQueryObserverOptions or InfiniteQuerySelectOptions) | required | Key, page function and paging functions. Re-applied whenever the parent rebuilds this widget. |
builder | Widget Function(BuildContext context, InfiniteQueryController<TPageData, TPageParam, TData> query) | required | Given the controller, not a bare result: the pages are in query.value, paging is query.fetchNextPage and its siblings. |
buildWhen | BuildWhen<QueryResult<TData>>? | null | Compares the controller's results. A change of the paging flags alone rebuilds regardless. |
client | QueryClient? | null | As for QueryBuilder. TanStack: the hook's queryClient argument. |
key | Key? | null | The 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.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
options | MutationOptions<TData, TVariables, TOnMutateResult> | required | The 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. |
builder | Widget Function(BuildContext context, MutationController<TData, TVariables, TOnMutateResult> mutation) | required | Given the controller: the result is mutation.value, and mutate or mutateAsync starts a run. |
buildWhen | BuildWhen<MutationResult<TData, TVariables>>? | null | The only filter a mutation reader has; it has no select. |
client | QueryClient? | null | The client to run on. TanStack: the hook's queryClient argument. |
key | Key? | null | The 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>>.
| Member | Type | Default | Meaning |
|---|---|---|---|
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. |
client | QueryClient | — | The client the observer runs on. |
value | QueryResult<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: true | Refetches. true cancels a fetch in flight and starts again; false joins it. TanStack: refetch. |
observer | QueryObserver<TQueryData, TData> | — | The observer underneath, for what the controller does not mirror, such as currentQuery. |
isDisposed | bool | — | Whether dispose has run. |
observedState | Object? | — | 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. |
optimisticValue | QueryResult<TData> (@protected) | — | The result the observer would report on subscribing now; what value reports while nobody listens. For subclasses. |
addListener / removeListener | void | — | 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.
| Member | Type | Default | Meaning |
|---|---|---|---|
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. |
hasNextPage | bool | — | getNextPageParam returns a param for the pages held. False before the first page. TanStack: hasNextPage. |
hasPreviousPage | bool | — | getPreviousPageParam returns a param. Always false without one. TanStack: hasPreviousPage. |
isFetchingNextPage | bool | — | The fetch in flight is a fetchNextPage. TanStack: isFetchingNextPage. |
isFetchingPreviousPage | bool | — | The fetch in flight is a fetchPreviousPage. TanStack: isFetchingPreviousPage. |
isFetchNextPageError | bool | — | The result's error came from a fetchNextPage; the pages held are still there. TanStack: isFetchNextPageError. |
isFetchPreviousPageError | bool | — | The error came from a fetchPreviousPage. TanStack: isFetchPreviousPageError. |
isRefetching | bool | — | The pages held are being refetched, and no fetchNextPage or fetchPreviousPage is in flight. TanStack: isRefetching. |
isRefetchError | bool | — | A refetch of the held pages failed, as opposed to a page fetch. TanStack: isRefetchError. |
fetchNextPage({cancelRefetch}) | Future<QueryResult<TData>> | cancelRefetch: true | Fetches the page after the ones held. TanStack: fetchNextPage. |
fetchPreviousPage({cancelRefetch}) | Future<QueryResult<TData>> | cancelRefetch: true | Fetches the page before the ones held. TanStack: fetchPreviousPage. |
infiniteObserver | InfiniteQueryObserver<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>>.
| Member | Type | Default | Meaning |
|---|---|---|---|
MutationController(client, options) | constructor; options is MutationOptions<TData, TVariables, TOnMutateResult> | — | Creates a MutationObserver. Idle until a run starts. TanStack: new MutationObserver(client, options). |
client | QueryClient | — | The client the observer runs on. |
value | MutationResult<TData, TVariables> | — | The current result. |
mutate(variables, {callbacks}) | void; callbacks is MutateCallbacks<TData, TVariables, TOnMutateResult>? | callbacks: null | Fire and forget: the result lands in value, errors never reach the caller. TanStack: mutate. |
mutateAsync(variables, {callbacks}) | Future<TData> | callbacks: null | Completes 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. |
observer | MutationObserver<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. |
isDisposed | bool | — | Whether dispose has run. |
addListener / removeListener | void | — | 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.
| Member | Returns | Parameters | Meaning |
|---|---|---|---|
query<TData>(options, {id, buildWhen}) | QueryResult<TData> | QueryObserverOptions<TData> options, Object? id, BuildWhen<QueryResult<TData>>? buildWhen | The 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>>? buildWhen | query 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>>? buildWhen | The 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>>? buildWhen | A 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
ifis fine. Two readers of one key share the query in the cache but not an observer. idtakes the key's place in the read's identity (the types stay part of it). A read with anidkeeps its observer when its key changes, which is what keeping the previous key's data on screen with a placeholder needs.idalso 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 itsmutationKey, each with its three types, else by the types alone. Two reads of one identity in one build share one controller, so without anida debug assertion fires when they differ in the mutation function (mutationFnormutationFnWithContext), inonMutate,onSuccess,onErrororonSettled, or inscope,retry,retryDelay,networkModeorgcTime. Those five compare by value, exceptRetryPolicy.whenandRetryDelay.dynamic, which compare by variant only.metais not compared. Only reads in aStatelessWidget's orState's own build are compared; a nested builder re-reading through the outercontext, and a read through aLayoutBuilder'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.
| Situation | What happens |
|---|---|
| A key read in the last build but not in this one | Released after the frame. A mutation too. |
| The widget unmounts | Everything it read is released. Releasing a mutation's controller does not cancel a run in flight. |
| A widget stops reading altogether | No 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 callback | Additive: 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 builder | Starts 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 context | Rebuilds 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 builder | Debug 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 changes | Every 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.
| Member | Returns | Parameters | Meaning |
|---|---|---|---|
queryClient | QueryClient (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>>? buildWhen | Subscribes 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>>? buildWhen | watchQuery 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>>? buildWhen | The 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>>? buildWhen | A 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
Stateis disposed. AStatethat stops reading altogether keeps its last observers until it is disposed. - A
watchQueryinside a nested builder callback — aValueListenableBuilder,LayoutBuilder,AnimatedBuilder, or aListView.builder'sitemBuilder— reads for thisStateand is additive. It is not refused in debug builds. A key the callback stops reading stays until an ownbuildof thisStatethat 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 callingwatchQueryis 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.
previousis what is on screen: the result the reader last built from. When the predicate returnsfalse, that staysprevious, and the next change is compared against it.selectnarrows the data a reader sees;buildWhennarrows when it rebuilds. It is the tool for a changeselectcannot see: a background refetch movesfetchStatusanddataUpdatedAt, 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,
QueriesBuilderandQueriesControllertake none. A controller is the notifier, and a predicate on it would impose one listener's filter on every listener. SeebuildWhen.
ListenWhen
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.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
controller | QueryController<TQueryData, TData> / InfiniteQueryController<TPageData, TPageParam, TData> / MutationController<TData, TVariables, TOnMutateResult> | required | The 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. |
listener | void Function(BuildContext context, T result) | required | The side effect. T is QueryResult<TData> for the two query listeners and MutationResult<TData, TVariables> for MutationListener. |
listenWhen | ListenWhen<T>? | null (every change) | Which transitions run listener. |
child | Widget | required | Returned unchanged; a transition never rebuilds it. |
key | Key? | null | The 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.listenWhenis asked in the same microtask. - Once per notification. Two cache writes inside one
notifyManager.batchare 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
listeneris reported throughFlutterError, 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.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
queries | List<QueryObserverOptionsBase<TQueryData, TData>> | required | The 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. |
builder | Widget Function(BuildContext context, List<QueryResult<TData>> results) | required | Built from one result per entry of queries, first and again whenever a result or the list changes. |
client | QueryClient? | null (provider's) | As for QueryBuilder. TanStack: the hook's queryClient argument. |
key | Key? | null | The 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.
| Member | Type | Default | Meaning |
|---|---|---|---|
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). |
client | QueryClient | — | The client every query in the collection uses. |
value | List<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. |
observer | QueriesObserver<TQueryData, TData> | — | The core observer, including its underlying observers. |
addListener / removeListener | void | — | 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.
| Member | Type | Default | Meaning |
|---|---|---|---|
IsFetchingController(client, {filters}) | constructor | filters: const QueryFilters() (all queries) | Creates the count over client's query cache. TanStack: useIsFetching(filters). |
client | QueryClient | — | The client whose queries are counted. |
filters | QueryFilters | all queries | Which queries count. Fixed for the controller's life; other filters are another controller. TanStack: filters. |
value | int | — | client.isFetching(filters: filters). |
addListener / removeListener | void | — | 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.
| Member | Type | Default | Meaning |
|---|---|---|---|
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. |
client | QueryClient | — | The client whose mutations are selected. |
value | List<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 / removeListener | void | — | 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.
| Variant | Class | Fields | Meaning |
|---|---|---|---|
OnlineStatus.fixed(bool online) | OnlineStatusFixed | online | This 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}) | OnlineStatusStream | changes, initial | Follow 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):
initialis 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 bothOnlineStatus.stream: a changedinitialon 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'sonListen): that event is believed overinitial.- 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
initialif 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 inbuilddoes not flicker. Afixedstatus 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 (
nullon 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
FlutterErrorpointing toasBroadcastStream(). A stream error is reported throughFlutterError.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.