Skip to content

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.

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;
}

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 result it starts from, or for a result equal to the previous one.
  • After update moves the slot to another observer, it starts over from the new result without 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.

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:

  1. Wrap the listener in notifyManager.batchCalls, so changes arrive in a microtask.
  2. Ignore the changes that arrive after dispose.

QueriesSlot and MutationStateSlot already call them in a microtask, so they need neither step.

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.

MutationStateSlot gives the state of every run of a mutation, wherever it started. The MutationState widgets and useMutationState use it.

  • It takes a MutationStateSource: a Mutation with a mutationKey, or MutationFilters.
  • It only reads the cache. Its observer is the client’s MutationCache.
  • Its result lists the runs’ states, oldest first. It stays the same list until a matching run is added, removed, or changes.
  • It calls subscribe listeners in a microtask, once per batch, and only when the list changed, so they need no batchCalls.

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.

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.
  • observer is null for a QueryResult built with its constructor.
  • An InfiniteQueryResult has an InfiniteQueryObserver, which an InfiniteQuerySlot takes.

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.

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.