graphql_ws_flutter
Flutter integration for graphql_ws.
Keeps a GraphQL over WebSocket connection alive across app lifecycle changes
and flaky mobile connectivity — so reconnection is seamless and your widgets
never see an error for a transient blip.
Install
dependencies:
graphql_ws: ^0.1.0
graphql_ws_flutter: ^0.1.0
Quick start
import 'package:graphql_ws/graphql_ws.dart';
import 'package:graphql_ws_flutter/graphql_ws_flutter.dart';
final client = createClient(
url: () => Uri.parse('wss://example.com/graphql'),
retryAttempts: 20, // a generous budget
retryWait: connectivityAwareRetryWait(), // park retries while offline
);
runApp(
GraphqlWsConnectionScope( // reconnect after backgrounding
client: client,
child: const MyApp(),
),
);
Those three highlighted choices are the whole story. The rest of this README explains why each one matters.
Handling retries & flaky connectivity
Mobile connections drop constantly — tunnels, elevators, lifts, dead zones, wifi↔cellular handoffs, the OS suspending a backgrounded app. Here is exactly how this library handles each case, and what you need to configure.
What graphql_ws already does for you
graphql_ws reconnects automatically. On a disconnect it retries (up to
retryAttempts, default 5) with exponential backoff and resubscribes
every active subscription. Your Stream / Sink does not receive an
error while this happens — it pauses, then resumes. An error only reaches
subscribers once the retry budget is fully exhausted. The budget resets to
zero on every successful connection, so a client that recovers gets a fresh
budget for the next blip.
That covers ordinary disconnects — a server restart, a brief network drop. Two mobile-specific problems remain, and that is what this package adds.
Problem 1 — the socket is dead but the client doesn't know it
When the OS suspends a backgrounded app, it freezes or silently kills the WebSocket. The socket still reports itself "open", no close event fires, so the retry machinery is never triggered. On resume the app looks connected but no data flows — a zombie socket.
GraphqlWsConnectionScope fixes this. On resume it actively probes the
socket (sends a ping, waits for a pong) and, if the socket is dead, calls
client.terminate() — which kicks the normal retry-and-resubscribe path. It
also watches connectivity, so a wifi↔cellular handoff reconnects too.
Problem 2 — retries burn out during an outage
While the device is offline, every reconnect attempt fails immediately. With the default backoff, a budget of 5 is spent in about a minute — then your subscriptions error out, even though the user just walked into a tunnel for two minutes.
connectivityAwareRetryWait fixes this. While the device is offline it
parks the retry — the attempt simply waits for connectivity to return
instead of failing. A 20-minute outage consumes a single retry slot, not
twenty. The instant the network is back, it reconnects and resubscribes.
Putting it together
final client = createClient(
url: () => Uri.parse('wss://example.com/graphql'),
// A budget for genuine, repeated failures. Because connectivityAwareRetryWait
// parks (rather than spends) attempts while offline, this is no longer a
// budget for *time* offline — so it can be generous.
retryAttempts: 20,
// Park retries while offline instead of burning the budget.
retryWait: connectivityAwareRetryWait(),
);
runApp(GraphqlWsConnectionScope(client: client, child: const MyApp()));
| Scenario | Handled by |
|---|---|
| Server restart, brief network drop | graphql_ws built-in retry (retryAttempts + backoff) |
| App backgrounded → socket frozen ("zombie") | GraphqlWsConnectionScope — resume probe |
| Wifi ↔ cellular handoff | GraphqlWsConnectionScope — connectivity observer |
| Extended offline stretch (tunnel, airplane mode) | connectivityAwareRetryWait — parks retries |
What your UI sees
Through all of the above your subscription streams do not emit an error —
they pause and resume. A StreamBuilder keeps showing its last value while
reconnecting; you usually need to do nothing. To show a "reconnecting…"
indicator, listen to lifecycle events:
client.on<ClientEvent>((event) {
switch (event) {
case ConnectingEvent(isRetry: true): // show "reconnecting…"
case ConnectedEvent(): // hide it
case _:
}
});
A subscription error surfaces only when retryAttempts is genuinely
exhausted — a sustained failure that is not merely being offline (e.g. a
server that keeps rejecting the connection). Treat that as a real "give up"
signal: show an error, offer a manual retry.
How resume detection works
On resume, GraphqlWsConnectionManager measures how long the app was
backgrounded:
- brief (<
trustWindow, default 5s) — do nothing, the socket survived. - medium — send a
pingand start a watchdog; if nopong(nor a fresh connection) arrives withinverificationTimeout(default 4s),terminate(). - long (>
deadAfter, default 3min) —terminate()immediately, the socket is certainly dead.
Tuning
Every knob is optional. Pass them to GraphqlWsConnectionScope (or
GraphqlWsConnectionManager):
| Option | Default | Purpose |
|---|---|---|
trustWindow |
5s | Backgrounded shorter than this → trust the socket. |
deadAfter |
3min | Backgrounded longer than this → terminate without probing. |
verificationTimeout |
4s | How long to wait for a pong during the resume probe. |
terminateOnPause |
false |
Also terminate when the app is paused (clean shutdown; costs a reconnect on every foreground). |
onRecovery |
— | Diagnostic callback — fires with a RecoveryTrigger whenever a reconnect is triggered. |
connectivitySource |
ConnectivityPlusSource |
Connectivity provider; override to inject a fake in tests. |
Driving it yourself
GraphqlWsConnectionScope is a thin widget over GraphqlWsConnectionManager.
If you do not have a convenient widget tree, use the manager directly:
final manager = GraphqlWsConnectionManager(client: client)..attach();
// ...later:
manager.dispose(); // does NOT dispose the client
Example
See example/ for a self-contained, runnable demo — it starts an
in-process server, shows a live counter subscription, and lets you drop the
connection and watch it recover.
License
MIT — see LICENSE.
Libraries
- graphql_ws_flutter
- Flutter integration for
graphql_ws.