A lightweight, dependency-light client for the Sockeon
real-time framework. Sockeon speaks a tiny JSON-over-WebSocket protocol — every
frame is a JSON object shaped {"event": "<name>", "data": { ... }} — and this
package gives you an idiomatic, event-driven Dart API on top of it with
streams, futures and automatic reconnection.
Works in both pure Dart and Flutter (mobile, desktop and web) via
web_socket_channel.
- Connect over
ws://orwss://, with optional auth key and query parameters. - Event listeners:
on,once,off, and a*wildcard. emit(event, data)with client-side event-name validation.- Room helpers:
joinRoom/leaveRoomthat resolve on the server ack. - Streams for connection state, all incoming messages and errors.
- Automatic reconnection with exponential backoff and optional room re-join.
Add the dependency to your pubspec.yaml:
dependencies:
sockeon: ^1.0.0Then run dart pub get (or flutter pub get).
import 'package:sockeon/sockeon.dart';
Future<void> main() async {
final client = SockeonClient(
host: '127.0.0.1',
port: 6001,
// authKey: 'secret', // only if the server enforces an auth key
);
// Listen for application events.
client.on('welcome', (data) {
print('Server says: ${data['message']}');
});
await client.connect();
// Join a room and emit an event.
await client.joinRoom('general');
client.emit('message.send', {'message': 'Hello from Dart!'});
}You can also build a client straight from a URL:
final client = SockeonClient.fromUrl('wss://example.com:6001/ws', authKey: 'secret');Every message — in both directions — is a JSON text frame with exactly two fields:
{ "event": "message.send", "data": { "message": "hello" } }Rules enforced by the server (and validated client-side before sending):
eventmust be a non-empty string matching^[a-zA-Z0-9._-]+$.datais always an object. Pass{}(the default) when there is no payload.
There is no mandatory server greeting and the connection has no built-in
client ID — if your backend sends a welcome event with an ID, that is
application-specific. Keep-alive uses standard WebSocket ping/pong, which the
underlying socket handles for you.
// Named listener; returns a cancellable subscription.
final sub = client.on('message.new', (data) {
print('new message: ${data['text']}');
});
sub.cancel(); // remove just this listener
// One-shot listener.
client.once('ready', (data) => print('ready once'));
// Remove all listeners for an event.
client.off('message.new');
// Catch-all: receive the data of every incoming event.
client.on('*', (data) => print('any event: $data'));Built-in server events you may want to handle:
| Event | data shape |
|---|---|
error |
{ "message": String, "timestamp": int } |
room_joined |
{ "room": String, "namespace": String, "timestamp": int } |
room_left |
{ "room": String, "namespace": String, "timestamp": int } |
rate_limit_exceeded |
{ "error", "message", "original_event", "retry_after", "limit", "window", "type" } |
await client.joinRoom('general'); // default namespace '/'
await client.joinRoom('chat', namespace: '/app');
await client.leaveRoom('general');
print(client.joinedRooms); // {'chat'}joinRoom / leaveRoom emit join_room / leave_room and complete when the
server replies with room_joined / room_left (or after ackTimeout).
client.states.listen((state) => print('state: $state'));
client.messages.listen((msg) => print('${msg.event} -> ${msg.data}'));
client.errors.listen((err) => print('error: $err'));
print(client.isConnected);
print(client.state); // SockeonConnectionState.connectedBy default the client reconnects automatically with exponential backoff and re-joins any rooms it had joined:
final client = SockeonClient(
host: '127.0.0.1',
port: 6001,
reconnectOptions: const SockeonReconnectOptions(
enabled: true,
maxAttempts: 0, // 0 = unlimited
initialDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 30),
backoffMultiplier: 2,
rejoinRooms: true,
),
);
// Disable entirely:
// reconnectOptions: SockeonReconnectOptions.disabled,Calling disconnect() stops reconnection; connect() re-enables it.
class ChatScreen extends StatefulWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late final SockeonClient _client;
final _messages = <String>[];
@override
void initState() {
super.initState();
_client = SockeonClient(host: '10.0.2.2', port: 6001) // 10.0.2.2 = host from Android emulator
..on('message.new', (data) {
setState(() => _messages.add(data['text'] as String));
});
_client.connect();
}
@override
void dispose() {
_client.dispose();
super.dispose();
}
void _send(String text) => _client.emit('message.send', {'text': text});
@override
Widget build(BuildContext context) {
return StreamBuilder<SockeonConnectionState>(
stream: _client.states,
initialData: _client.state,
builder: (context, snapshot) {
final connected = snapshot.data?.isConnected ?? false;
return Scaffold(
appBar: AppBar(title: Text(connected ? 'Online' : 'Connecting…')),
body: ListView(children: [for (final m in _messages) ListTile(title: Text(m))]),
);
},
);
}
}On the Android emulator use
10.0.2.2to reach a server running on your host machine. Forwss://over the internet, setsecure: trueor useSockeonClient.fromUrl('wss://...').
| Member | Description |
|---|---|
SockeonClient({host, port, path, secure, authKey, queryParameters, reconnectOptions, protocols, ackTimeout}) |
Create a client. |
SockeonClient.fromUrl(url, {...}) |
Create a client from a ws:///wss:// URL. |
connect() |
Open the connection (completes on handshake). |
disconnect({code, reason}) |
Close gracefully and stop reconnecting. |
dispose() |
Release all resources permanently. |
emit(event, [data]) |
Send an event. |
on(event, handler) → SockeonSubscription |
Add a listener. |
once(event, handler) |
Add a one-shot listener. |
off(event, [handler]) |
Remove a listener (or all for an event). |
joinRoom(room, {namespace}) / leaveRoom(...) |
Manage rooms. |
states / messages / errors |
Broadcast streams. |
state / isConnected / joinedRooms |
Current status getters. |