inverge_websocket
Flutter client SDK for the Inverge WebSocket service.
Room-based realtime messaging over Socket.IO with an app-wide provider,
context.socket access from any page, automatic reconnect and room re-join,
ack futures, and a connection-status stream.
- 🌐 Service: inverge.net/services/websocket
- 📊 Dashboard (usage & billing): wss.inverge.net
Features
- 🔌 One-line app-wide setup with
WebSocketInitializer. - 📍
context.socketanywhere — plusInvergeWebSocket.instancefor blocs/services. - 🏠 Room helpers:
joinRoom/leaveRoom(and bulk), with automatic re-join after a reconnect (a fresh socket has no room membership — this fixes the classic "events stop after reconnect" bug). - ✅ Ack futures:
await socket.joinRoom(...),await socket.emitWithAck(...). - 📶
ConnectionStatusstream for reactive UI. - 🔁 Reconnect on app resume; optional disconnect on background.
- 🧭
Roomhelper to build recipient-scoped names (order:8821).
Install
Run this command:
$ dart pub add inverge_websocket
With Flutter:
$ flutter pub add inverge_websocket
This will add a line like this to your package's pubspec.yaml (and run an implicit dart pub get / flutter pub get):
dependencies:
inverge_websocket: ^1.1.0
Quick start
Wrap your app once near the root:
import 'package:inverge_websocket/inverge_websocket.dart';
WebSocketInitializer(
config: const WebSocketConfig(
baseUrl: 'https://websocket.inverge.net',
apiKey: 'your_api_key',
),
child: MaterialApp.router(routerConfig: appRouter),
);
Then use it from any page via context.socket:
@override
void initState() {
super.initState();
context.socket.joinRoom(Room.scoped('order', order.id));
context.socket.on('order.updated', _onUpdate);
}
@override
void dispose() {
context.socket.off('order.updated', _onUpdate);
context.socket.leaveRoom(Room.scoped('order', order.id));
super.dispose();
}
From a bloc/service (no BuildContext), use the singleton:
InvergeWebSocket.instance.emit('typing', {'orderId': 8821});
Rooms
final socket = context.socket;
await socket.joinRoom('order:8821'); // returns the server ack
await socket.joinRooms(['order:8821', 'ops']);
await socket.leaveRoom('order:8821');
socket.joinedRooms; // currently tracked rooms
Joined rooms are remembered and re-joined automatically after a reconnect
(disable with rejoinRoomsOnReconnect: false). Calling joinRoom while
disconnected is fine — it joins on the next connect.
Scope rooms by recipient, not by topic. A room is a delivery list — join a room only if you want every message sent there. Use
Room.scoped('order', id)rather than one big room filtered by event name.
Events & acks
socket.on('order.updated', (data) { /* ... */ });
socket.once('welcome', (data) { /* ... */ });
socket.off('order.updated'); // remove all handlers for event
socket.emit('typing', {'orderId': 8821}); // fire-and-forget
final ack = await socket.emitWithAck('ping', {}); // await the server ack
Connection status
StreamBuilder<ConnectionStatus>(
stream: context.socket.statusStream,
initialData: context.socket.status,
builder: (context, snap) => Text(snap.data!.name),
);
context.socket.isConnected; // bool
context.socket.id; // current socket id (or null)
Configuration
| Option | Default | Description |
|---|---|---|
baseUrl |
— | Service origin, e.g. https://websocket.inverge.net. |
apiKey |
— | Tenant key, sent in the handshake auth payload. |
transports |
['websocket','polling'] |
Socket.IO transports. |
autoConnect |
true |
Connect on initialize. |
reconnection |
true |
Enable Socket.IO reconnection. |
reconnectionAttempts |
5 |
Max attempts. |
reconnectionDelay |
1000ms |
Delay between attempts. |
connectTimeout |
20s |
Handshake timeout. |
ackTimeout |
10s |
Default timeout for ack emits. |
rejoinRoomsOnReconnect |
true |
Re-join tracked rooms after reconnect. |
enableLogging |
false |
Log lifecycle/room ops via debugPrint. |
extraAuth |
{} |
Extra fields merged into the handshake auth. |
sendPlatformInfo |
true |
Report the client OS type + version at connection time so the service can identify / filter each session by platform. |
query |
{} |
Extra connection query params. |
Lifecycle & ownership
WebSocketInitializer(config: ...) creates and owns the client — it
connects on start, reconnects on resume, and disposes on teardown. To manage the
client yourself, build it and pass it in:
final client = InvergeWebSocket.initialize(config);
WebSocketInitializer.client(client: client, child: app);
Native emit (Android & iOS background)
The socket client above lives in the Dart/UI layer. For backend-style emits
from native code — an Android foreground LocationService, an iOS background
task — where a live socket (and even the Flutter engine) may be gone, use the
native HTTP emitter. It posts to the same /partner/rooms/... API the server
SDKs use, is dependency-free (Android HttpURLConnection, iOS URLSession), and
its credentials can be persisted so a background service restores them.
Configure once from Dart (persists to native storage):
await WebSocketNative.configure(
baseUrl: 'https://websocket.inverge.net',
apiKey: 'your_api_key',
); // persist: true by default -> Android SharedPreferences("native_env"), iOS UserDefaults
// Emit from Dart via the native HTTP path (one or more events):
await WebSocketNative.emit('order:8821', 'location', payload: {'lat': 1.0, 'lng': 2.0});
await WebSocketNative.emit('order:8821', ['location', 'audit'], payload: {'lat': 1.0});
// Emit to SEVERAL rooms in one request with broadcast(). Each message accepts
// `room`/`name`, `event` (String) or `events` (List<String>), and `payload`:
final result = await WebSocketNative.broadcast([
{'room': 'order:8821', 'event': 'location', 'payload': {'lat': 1.0, 'lng': 2.0}},
{'room': 'ops', 'events': ['location'], 'payload': {'orderId': 8821}},
]);
debugPrint('broadcast -> ${result.statusCode} ok=${result.success}'); // NativeEmitResult
Emit from native Android (Kotlin) — e.g. inside a foreground service, off the main thread:
import net.inverge.websocket.InvergeWebSocket
// Restore the credentials Dart persisted (safe to call repeatedly):
InvergeWebSocket.loadFrom(applicationContext)
InvergeWebSocket.emit(
room = "order:$orderId",
event = "location",
payload = mapOf(
"lat" to location.latitude,
"lng" to location.longitude,
"speed" to location.speed,
"accuracy" to location.accuracy,
"timestamp" to location.time,
),
) { result ->
if (!result.success) Log.w("LocationService", "emit failed: ${result.error}")
}
// MULTIPLE EVENTS to one room — pass a list:
InvergeWebSocket.emit(
room = "order:$orderId",
events = listOf("location", "audit"),
payload = mapOf("lat" to lat, "lng" to lng),
)
// MULTIPLE ROOMS in one request — broadcast(). Each message map takes
// "room"/"name", "event" (String) or "events" (List<String>), and "payload":
InvergeWebSocket.broadcast(
listOf(
mapOf("room" to "order:$orderId", "event" to "location", "payload" to mapOf("lat" to lat, "lng" to lng)),
mapOf("room" to "ops", "events" to listOf("location"), "payload" to mapOf("orderId" to orderId)),
),
) { result ->
Log.d("LocationService", "broadcast -> ${result.statusCode} ok=${result.success}")
}
// Or, inside an existing coroutine / worker thread, the blocking form
// (single room, one or more events):
val result = InvergeWebSocket.emitBlocking(
room = "order:$orderId",
events = listOf("location", "audit"),
payload = JSONObject().put("lat", lat).put("lng", lng),
)
Emit from native iOS (Swift):
InvergeWebSocket.shared.loadFromDefaults()
// Single event:
InvergeWebSocket.shared.emit(room: "order:\(orderId)", event: "location",
payload: ["lat": lat, "lng": lng])
// MULTIPLE EVENTS to one room:
InvergeWebSocket.shared.emit(room: "order:\(orderId)", events: ["location", "audit"],
payload: ["lat": lat, "lng": lng])
// MULTIPLE ROOMS in one request — broadcast(). Each message takes
// "room"/"name", "event" (String) or "events" ([String]), and "payload":
InvergeWebSocket.shared.broadcast([
["room": "order:\(orderId)", "event": "location", "payload": ["lat": lat, "lng": lng]],
["room": "ops", "events": ["location"], "payload": ["orderId": orderId]],
]) { result in
NSLog("broadcast -> \(result.statusCode) ok=\(result.success)")
}
This replaces hand-rolled HttpURLConnection/URLSession emit code in native
services with one shared, correct implementation.
Native emit is Android/iOS only; the Dart
WebSocketNativecalls throwMissingPluginExceptionon other platforms.
Full method reference
Every public method, with a usage example.
WebSocketConfig
// Construct (only baseUrl + apiKey are required):
const config = WebSocketConfig(
baseUrl: 'https://websocket.inverge.net',
apiKey: 'your_api_key',
transports: ['websocket', 'polling'],
autoConnect: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: Duration(milliseconds: 1000),
connectTimeout: Duration(seconds: 20),
ackTimeout: Duration(seconds: 10),
rejoinRoomsOnReconnect: true,
enableLogging: false,
extraAuth: {'deviceId': 'abc'}, // merged into the handshake auth
query: {'v': '1'}, // extra connection query params
);
// Copy with overrides:
final verbose = config.copyWith(enableLogging: true);
WebSocketInitializer (widget)
// Owns the client (connects, reconnects on resume, disposes on teardown):
WebSocketInitializer(
config: config,
reconnectOnResume: true, // reconnect when app returns to foreground
disconnectOnBackground: false, // keep the socket while backgrounded
child: myApp,
);
// Or provide a client you built yourself:
WebSocketInitializer.client(client: myClient, child: myApp);
InvergeWebSocket (client)
// Create the app-wide instance (used by WebSocketInitializer under the hood):
final ws = InvergeWebSocket.initialize(config);
// App-wide access from non-widget code (blocs/services):
InvergeWebSocket.instance; // the client (throws if not initialized)
InvergeWebSocket.hasInstance; // bool — safe to check first
// Connection lifecycle:
ws.connect(); // open (no-op if already connected)
ws.disconnect(); // close but keep reusable (rooms remembered)
ws.dispose(); // permanent teardown (closes the status stream)
// State getters:
ws.isConnected; // bool
ws.status; // ConnectionStatus
ws.id; // String? current socket id
ws.statusStream; // Stream<ConnectionStatus>
ws.joinedRooms; // Set<String> tracked rooms
ws.socket; // the raw socket_io_client Socket (advanced)
ws.config; // the WebSocketConfig in use
// Rooms (all return the server ack future):
await ws.joinRoom('order:8821');
await ws.leaveRoom('order:8821');
await ws.joinRooms(['order:8821', 'ops']);
await ws.leaveRooms(['order:8821', 'ops']);
// Events:
ws.on('order.updated', (data) { /* ... */ }); // subscribe
ws.once('welcome', (data) { /* ... */ }); // subscribe once
ws.off('order.updated'); // remove all handlers for event
ws.off('order.updated', myHandler); // remove one handler
// Emitting:
ws.emit('typing', {'orderId': 8821}); // fire-and-forget
final ack = await ws.emitWithAck('ping', {}); // await server ack
final ack2 = await ws.emitWithAck('ping', {}, Duration(seconds: 3)); // custom timeout
ConnectionStatus (enum)
ConnectionStatus.disconnected;
ConnectionStatus.connecting;
ConnectionStatus.connected;
ConnectionStatus.reconnecting;
ConnectionStatus.connected.isConnected; // true; false for the others
Room (scoped names)
Room.scoped('order', 8821); // 'order:8821'
Room.of('pilot', 17); // 'pilot:17' (alias of scoped)
Room.path(['order', 8821, 'chat']); // 'order:8821:chat'
WebSocketNative (Android/iOS HTTP emit)
// Configure once (persist:true stores creds in native storage):
await WebSocketNative.configure(
baseUrl: 'https://websocket.inverge.net',
apiKey: 'your_api_key',
persist: true,
);
// Emit one event, or a list of events:
final r1 = await WebSocketNative.emit('order:8821', 'location', payload: {'lat': 1.0});
final r2 = await WebSocketNative.emit('order:8821', ['location', 'audit']);
// Emit to several rooms in one request:
final r3 = await WebSocketNative.broadcast([
{'room': 'order:8821', 'event': 'location', 'payload': {'lat': 1.0}},
{'room': 'ops', 'events': ['location'], 'payload': {'orderId': 8821}},
]);
// NativeEmitResult:
r3.success; // bool
r3.statusCode; // int (HTTP status)
r3.body; // String? response body
r3.error; // String? error code/message when !success
Migrating from a hand-rolled manager
Replace your singleton WebSocketManager + context.socket with this package:
WebSocketManager.instance→InvergeWebSocket.instance(orcontext.socket).- Hardcoded
Env.socketBaseUrl/Env.socketApiKey→WebSocketConfig. - Room joins now survive reconnects automatically, and join/leave return futures.
License
MIT.
Libraries
- inverge_websocket
- Flutter client SDK for the Inverge WebSocket service.