15 — Flutter Implementation Guide (WS / Realtime Module)
- 1. Transport choice — read this first (honest note)
- 2. Connection setup
- 3. Envelope handling
- 4. Auto-reconnect with backoff (state machine in
13§3) - 5. Heartbeat
- 6. Bloc integration
- 7. Offline UX wiring
- 8. Security checklist
- 9. Testing
How to implement the realtime client in Flutter. Shared architecture: 00-shared/11 (Flutter App Architecture). State machine it must implement: 13.
1. Transport choice — read this first (honest note)
The server is socket.io (ws.gateway.ts:10-11), which speaks the engine.io
handshake + protocol, not plain RFC 6455 WebSocket. Two options:
| Option | Package | Fit |
|---|---|---|
| A — recommended | socket_io_client | Full protocol parity: engine.io handshake, long-polling fallback, built-in reconnect, auth.token field, namespaces (/ws) |
| B | web_socket_channel (raw WS) | Only works if the server runs with transports: ['websocket'] only and the client reproduces the engine.io sid handshake dance — fragile; not recommended for production |
This guide uses web_socket_channel as requested, but the raw-WebSocket path must
be validated against the engine.io upgrade flow before adoption; if parity is not
achievable, switch to option A — the RealtimeClient abstraction (07 §6) makes the
swap a one-file change.
2. Connection setup
final uri = Uri.parse(
'$WS_SCHEME://$WS_HOST/ws/socket.io/?token=$token' // query.token path (ws.gateway.ts:37-39)
);
final channel = IOWebSocketChannel.connect(uri);
- Prefer the query token with
web_socket_channel(noauthmap available on raw sockets;socket_io_clientcan useauth: {'token': ...}instead — both accepted by source:ws.gateway.ts:37-39). WS_SCHEME=wssin prod; namespace path/ws(ws.gateway.ts:20-23).- Token from the secure auth store; never log the URI (contains token,
10§8). - Verify failure server-side = close (
ws.gateway.ts:54-56) — treat any earlydoneduring handshake asAuthErroruntil proven otherwise (13§1).
3. Envelope handling
class WsEnvelope {
final String eventType;
final DateTime occurredAt;
final Map<String, dynamic> payload;
// validate exactly these 3 keys — anything else = protocol violation (06 §6)
}
Server emits {eventType, occurredAt, payload} (ws-bridge.service.ts:17-21). Route
via the pure RealtimeBloc router (13 §2). Ignore unknown eventTypes.
4. Auto-reconnect with backoff (state machine in 13 §3)
Future<void> _runReconnectLoop() async {
var attempt = 0;
while (_shouldRetry) {
await Future<void>.delayed(backoffDelay(attempt)); // 1s * 2^attempt, cap 30s
final ok = await _connectOnce();
if (ok) return;
attempt++;
if (attempt >= 5 && !await _refreshToken()) { // auth on reconnect (Q2)
_emit(RealtimeAuthError()); return; // → sign-out path (06 §1.2)
}
}
_emit(RealtimeOffline()); // manual "Retry now" resumes
}
Duration backoffDelay(int n) {
final base = min(1 << n, 30) * 1.seconds; // exponential, cap 30s
return base + Random().nextInt(250).milliseconds; // jitter → no storms (Q1)
}
Rules:
- On every successful reconnect: re-
subscribeextra rooms (rooms die with the socket,ws.gateway.ts:63-68) then run one reconcile refetch per visible live screen (06§3.1). - Token expiry check before reconnecting (
06§6); refresh via REST when needed.
5. Heartbeat
- Server side runs socket.io ping/pong (defaults; untuned, G6). On raw WS, you must
detect silent half-open loss yourself: no frame (incl. pong) for
pingTimeout+grace → close channel →Reconnecting.IOWebSocketChanneldoes not heartbeat — add aTimerwatchdog if using option B (14Q4).
6. Bloc integration
class RealtimeBloc extends Cubit<RealtimeState> {
RealtimeBloc(this._client) {
_sub = _client.events.listen(_route); // envelope → typed app events (13 §2)
_client.state.listen(emit);
}
// close(): cancel _sub — no fan-out leaks after navigation (Q5)
}
Feature blocs subscribe to router output only; AppLiveList (07 §3) owns merge
semantics; never mutate lists during active edits (10 §2).
7. Offline UX wiring
AppRealtimeStatusDot+AppConnectionBannerbind toRealtimeBloc.state(11§3); banner only foroffline/authError; live-region announcements (00-shared/09).- Status sheet
(proposed)reads last-envelope metadata held by the bloc (13§5).
8. Security checklist
- Token only from secure storage; URI built at runtime (never in code/logs).
- No payload content in logs (
10§8); debug view redacts (09§5). - Validate envelope shape before routing (
14Q8); cap payload size. - Reject/ignore any server message that isn't a valid envelope.
9. Testing
RealtimeClientbehind a fake for widget tests (Q2, Q5, Q7).- Backoff unit test: delays bounded, jittered, no duplicates (Q1).
- Integration:
socket_io_clientagainst the real e2e server (npm run test:e2e,14test-surface table).