Offline & Network
Most libraries treat "offline" as one more error. Qora treats it as a state: reads pause instead of failing, writes can be queued, and both resume on their own when the network returns. This page describes the model; the Network-Aware recipe covers configuration in detail.
How Qora knows the device is offline
The client listens to a ConnectivityManager. QoraScope.auto attaches FlutterConnectivityManager, built on connectivity_plus, so a standard Flutter setup needs nothing more.
Two consequences worth knowing:
- Without a connectivity manager, the client assumes it is online. Nothing pauses, and nothing replays on reconnect. A pure Dart app can attach its own with
client.attachConnectivityManager(...). - Connectivity means a network interface, not a reachable server. On a captive portal or with the API down, the device counts as online: requests run, are retried, and end in
Failure.
Two axes: what you have, and what is happening
A query has a state and a fetch status, and offline handling lives in the second one:
QoraStateis what you have:Initial,Loading,Success,Failure, each carrying data when there is some.FetchStatusis what the engine is doing:fetching,paused(waiting for the network) oridle.
QoraBuilder passes both to your builder. The usual offline combinations:
| State | fetchStatus | Meaning |
|---|---|---|
Loading, previousData set | paused | Offline, cached data available, refetch waiting |
Loading, no previousData | paused | Offline and nothing cached: show an offline screen, not a spinner |
Success | paused | Offline in offlineFirst mode: cached data served, refetch waiting |
Success | idle | Fresh data, nothing to do |
A fetch started while offline does not throw inside widgets. Called imperatively, fetchQuery returns the cached data if there is some, and otherwise throws QoraOfflineException.
Reads: NetworkMode
networkMode decides what a query does when it must fetch while offline. Set it per query in QoraOptions, or for every query in defaultOptions.
| Mode | While offline | On reconnect |
|---|---|---|
online (default) | The fetch is not sent. State becomes Loading(previousData: cached), status paused | The fetch runs |
offlineFirst | With cached data: state stays Success, status paused. Without: same as online | The fetch runs |
always | The fetch is sent anyway, and is retried and fails like any error | Nothing special |
Use always for fetchers that do not need the network, such as a local database. Use offlineFirst when a screen should never flip to a loading state just because the device went offline.
Writes: the offline mutation queue
A mutation does not pause. Without opt-in, a mutation called while offline sends its request and fails. With offlineQueue: true in its MutationOptions, it is queued instead:
mutate(variables) while offline
1. onMutate runs now → optimistic cache updates work offline
2. the call is added to the queue
3. state: MutationSuccess(isOptimistic: true) if optimisticResponse is set,
MutationPending otherwise
reconnect
4. the call is sent, once, in the order it was queued
success → onSuccess, invalidates, onSettled
failure → onError(error, variables, context from step 1), onSettled
Three properties matter in practice:
- The queue belongs to the client, not to the widget. A queued write is sent even if the screen that made it was closed.
- A failed replay is not retried.
onErrorruns with the snapshot fromonMutate, so the rollback you wrote for the online case also covers this one. Tell the user there. - Order is preserved, dependencies are not checked. If a queued "create" is rejected, a queued "update" of the same item is still sent, and fails in turn.
optimisticResponse only sets the mutation's own state, for example to show a "queued" badge. Changes to cached query data belong in onMutate.
QoraMutationBuilder and useMutation both use the queue (useMutation since qora_hooks 1.3.1). A raw MutationController needs isOnline and offlineQueue passed explicitly.Reconnect
When the connectivity manager reports the network is back, the client:
- Replays paused queries in batches.
ReconnectStrategysets the batch size and a random delay between batches (default: 5 at a time, up to 100 ms), so a hundred paused queries do not hit the server at once. - Then replays the mutation queue, after the reads, one mutation at a time.
Independently, QoraScope.auto also refetches stale on-screen queries when the app returns to the foreground.
Across restarts
| What | Survives a restart? |
|---|---|
| Cached query data | Yes, with PersistQoraClient and a StorageAdapter. See Persistence |
| Queued mutations | No. The queue lives in memory; persisting it is planned for 1.4 |
An app that must not lose writes made offline before being killed should, for now, also save those writes itself (for example to a local outbox table) and resend them at startup.
Next Steps
- Network-Aware recipe: configuration,
isOptimistic,ReconnectStrategy - Network Status widgets: offline banners
- Offline-First case study: a full app with persistence and a queue
- Mutations & Invalidation: the write lifecycle this page builds on