Building an adapter
An adapter renders Fuery’s queries and mutations in your own widgets or another state library, with only the public API of fuery_core. The widgets of fuery and the hooks of fuery_hooks are adapters built this way, so yours can do everything they do.
An adapter keeps one slot per rendered query or mutation. A slot, an ObserverSlot, holds the observer for a source that can change on every render.
Rendering a query with a slot
Section titled “Rendering a query with a slot”Call update on every render, read result, and subscribe to render again. This hook, written with flutter_hooks alone, holds the whole contract:
QueryResult<TData> useMyQuery<TData extends Object>(QuerySource<TData> query) { final client = FueryProvider.of(useContext(), listen: true); final slot = useMemoized(() => QuerySlot(query, client)); final changes = useState(0); useEffect(() { var active = true; final unsubscribe = slot.subscribe(notifyManager.batchCalls((_) { if (active) changes.value++; })); return () { active = false; unsubscribe(); slot.dispose(); }; }, [slot]); slot.update(query, client); return slot.result;}The slot contract
Section titled “The slot contract”QuerySlot takes a QuerySource: a Query or a QueryObserver. It has these members, and so do InfiniteQuerySlot and MutationSlot:
| Member | What it does |
|---|---|
update(source, client) |
Call it on every render. For a definition, the slot owns an observer and updates its options, and a new client gets a new observer. For an observer, the slot uses it as it is, with the client it was created with. |
result |
The result to render, current as soon as update returns. |
subscribe(listener) |
Calls listener with every later result, and stays subscribed when update moves the slot to another observer. Returns a function that removes it. |
listen((previous, current) {...}) |
Calls its listener after each later change, for side effects such as navigation. previous is the last result it passed to the listener, or the result it started from. Returns a function that stops it. |
dispose() |
Removes every listener. If the slot created the observer, it destroys a query observer or resets a mutation observer. The reset drops the callbacks of the latest mutate call. |
observer |
The observer the slot renders from now. |
The query, infinite query, and mutation listener and consumer widgets, useOnQueryChange, and useOnMutationChange call listen, so they follow its rules:
- It runs in a microtask, never during a render.
- It isn’t called for the
resultit starts from, or for a result equal to the previous one. - After
updatemoves the slot to another observer, it starts over from the newresultwithout a call. - It subscribes, so a query fetches as it would for a mounted widget.
- Fuery reports a listener that throws to the client’s
onUncaughtError.
Batching listener calls
Section titled “Batching listener calls”QuerySlot, InfiniteQuerySlot, and MutationSlot call subscribe listeners synchronously, sometimes while another widget builds, such as when a widget that mounts starts a fetch. In a framework that can’t update during a render, do what the hook above does:
- Wrap the listener in
notifyManager.batchCalls, so changes arrive in a microtask. - Ignore the changes that arrive after dispose.
QueriesSlot and MutationStateSlot already call them in a microtask, so they need neither step.
Other slots
Section titled “Other slots”| Slot | Source | Result |
|---|---|---|
InfiniteQuerySlot |
InfiniteQuerySource: an InfiniteQuery or an InfiniteQueryObserver |
InfiniteQueryResult |
MutationSlot |
MutationSource: a Mutation or a MutationObserver |
MutationResult |
QueriesSlot |
A list of QuerySources of one data type |
A list of QueryResults, in order |
QueriesSlot serves a hook like useQueries. It calls subscribe listeners in a microtask, once for the changes that arrive together, so they need no batchCalls. Each query keeps its observer while its key stays in the list, even when the list is reordered. Its observer is the list of observers, a new list only when one is added, removed, replaced, or moved.
Every run of a mutation
Section titled “Every run of a mutation”MutationStateSlot gives the state of every run of a mutation, wherever it started. The MutationState widgets and useMutationState use it.
- It takes a
MutationStateSource: aMutationwith amutationKey, orMutationFilters. - It only reads the cache. Its
observeris the client’sMutationCache. - Its
resultlists the runs’ states, oldest first. It stays the same list until a matching run is added, removed, or changes. - It calls
subscribelisteners in a microtask, once per batch, and only when the list changed, so they need nobatchCalls.
subscribeToRuns((previous, current) {...}) calls its listener for each later change of each matching run, with that run’s previous state: idle for a run that started later. It never reports the states runs had when it was added, or a run that the cache removes. MutationStateListener and useOnMutationStateChange use it.
Listening from a result alone
Section titled “Listening from a result alone”A result carries the observer that reported it, as result.observer. An adapter given only a result listens through a slot of its own over that observer, as useOnQueryChange and useOnMutationChange do:
void Function() listenTo<TData extends Object>( QueryResult<TData> result, void Function(QueryResult<TData> previous, QueryResult<TData> current) listener,) { final observer = result.observer; if (observer == null) return () {}; final slot = QuerySlot(observer, observer.client); slot.listen(listener); return slot.dispose;}- The slot uses the observer as it is, and never destroys it.
observeris null for aQueryResultbuilt with its constructor.- An
InfiniteQueryResulthas anInfiniteQueryObserver, which anInfiniteQuerySlottakes.
Reading the client
Section titled “Reading the client”In Flutter, FueryProvider.of(context, listen: true) returns the nearest provided client, or Fuery.client. The caller rebuilds when the provided client is replaced. The next update then moves a slot that owns its observer to the new client.
Outside Flutter, pass the client your app uses.
Refetching on focus and reconnect
Section titled “Refetching on focus and reconnect”In Flutter, call FueryBinding.ensureInitialized() when the adapter mounts, as Fuery’s widgets and hooks do. It connects the app lifecycle, so stale queries refetch when the app resumes and retries wait while the app is in the background. Calls after the first do nothing. A FueryProvider above calls it too. See When the app resumes.
A client refetches on focus and on reconnect, and resumes paused mutations, only while it is mounted. Fuery.client is always mounted, and a FueryProvider mounts its client. Call mount() on any other client your adapter uses, and unmount() when you stop using it. Outside Flutter, connect the host’s focus events with focusManager.setEventListener. See the QueryClient reference.
In the Fuery sources
Section titled “In the Fuery sources”adapter_test.dartrenders queries through slots without Flutter.hooks.dartbuilds every hook offuery_hookson a slot.result_subscriber.dartbuilds the widgets offueryon a slot.