OddSockets Flutter SDK
Official Flutter/Dart SDK for OddSockets real-time messaging platform
Overview & Features
The OddSockets Flutter SDK provides a powerful, easy-to-use interface for real-time messaging across all Flutter-supported platforms including iOS, Android, Web, Windows, macOS, and Linux.
Cross-platform
Works on iOS, Android, Web, Windows, macOS, and Linux with the same API.
Stream-based
Native Dart Stream integration for reactive programming with StreamBuilder.
BLoC Integration
Built-in support for flutter_bloc state management pattern.
High Performance
Optimized for low latency with efficient WebSocket connections and smart routing.
Battery Optimized
Mobile-optimized reconnection logic and background handling for better battery life.
Type Safety
Full Dart type safety with comprehensive error handling and recovery.
Installation
dependencies:
oddsockets_flutter: ^1.0.0
flutter pub add oddsockets_flutter
dart pub add oddsockets_flutter
Quick Start
Basic Usage
import 'package:oddsockets_flutter/oddsockets_flutter.dart';
final client = OddSocketsClient(
OddSocketsConfig.defaultConfig('ak_live_1234567890abcdef'),
);
final channel = client.channel('my-channel');
// Subscribe to messages
await channel.subscribe((message) {
print('Received: ${message.data}');
});
// Publish a message
await channel.publish('Hello, Flutter!');
StreamBuilder Integration
class ChatWidget extends StatefulWidget {
@override
_ChatWidgetState createState() => _ChatWidgetState();
}
class _ChatWidgetState extends State<ChatWidget> {
late OddSocketsClient client;
late OddSocketsChannel channel;
@override
void initState() {
super.initState();
client = OddSocketsClient(
OddSocketsConfig.defaultConfig('ak_live_1234567890abcdef'),
);
channel = client.channel('chat-room');
}
@override
Widget build(BuildContext context) {
return StreamBuilder<Message>(
stream: channel.messageStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Latest: ${snapshot.data!.data}');
}
return Text('Waiting for messages...');
},
);
}
}
Configuration Builder
final config = OddSocketsConfig.builder('ak_live_1234567890abcdef')
.mobile() // Mobile-optimized settings
.heartbeatInterval(Duration(seconds: 45))
.reconnectAttempts(10)
.build();
final client = OddSocketsClient(config);
Configuration
Client Options
final client = OddSocketsClient(
OddSocketsConfig(
apiKey: 'your-api-key', // Required: Your OddSockets API key
userId: 'user-id', // Optional: User identifier
autoConnect: true, // Optional: Auto-connect on creation
reconnectAttempts: 5, // Optional: Max reconnection attempts
heartbeatInterval: Duration(seconds: 30), // Optional: Heartbeat interval
),
);
Channel Options
await channel.subscribe(callback, SubscribeOptions(
enablePresence: true, // Enable presence tracking
retainHistory: true, // Retain message history
filterExpression: 'user.premium == true', // Message filter expression
));
await channel.publish(message, PublishOptions(
ttl: Duration(hours: 1), // Time to live
metadata: {'priority': 'high'}, // Additional metadata
storeInHistory: true, // Store in message history
));
Examples
Explore comprehensive examples demonstrating the OddSockets Flutter SDK in action:
Enhanced Features
Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface —
reactions, typing indicators, threads, read receipts, presence/status, notifications, DMs,
channel management, message editing and search. It lives on client.enhanced.
The pattern is always the same: send an action with a
client.enhanced.* method, then receive the paired broadcast
with client.on('<event>', handler).
Typing & Reactions
// Receive-path: broadcasts from other users on the channel
client.on('user_typing', (data) => print('${data['userId']} is typing'));
client.on('reaction_added', (data) => print('${data['userId']} reacted ${data['emoji']}'));
// Send-path: enhanced actions over the live socket
client.enhanced.startTyping('alice', 'room-42');
client.enhanced.addReaction(
messageId: 'msg-1',
channel: 'room-42',
emoji: ':thumbsup:',
userId: 'alice',
userName: 'Alice',
);
Threads
client.on('thread_reply', (data) => print('new thread reply'));
final reply = await client.enhanced.threadReply(
channel: 'room-42',
parentMessageId: 'msg-1',
message: 'Replying in the thread',
userId: 'alice',
userName: 'Alice',
);
print('thread reply: $reply');
Enhanced surface
Each area exposes methods on client.enhanced; the worker broadcasts the
paired events which you handle with client.on(...). Query methods
(get*, search*) return a
Future<Map<String, dynamic>> that completes with the worker response.
- Typing —
startTyping,stopTyping→user_typing,user_stopped_typing - Reactions —
addReaction,removeReaction,getReactions→reaction_added,reaction_removed - Threads —
threadReply,getThread,subscribeThread,followThread,markThreadRead→thread_reply,thread_subscribed,thread_followed,thread_read_updated - Read receipts —
markRead,markAllRead,getUnreadCounts→user_read,unread_count_updated,all_marked_read - Messages —
editMessage,deleteMessage,pinMessage,unpinMessage,getPinnedMessages,searchMessages→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
setStatus,setCustomStatus,setDND,getUserPresence→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
createChannel,updateChannel,archiveChannel,inviteToChannel,joinChannel,leaveChannel→channel_created,channel_updated,user_invited,user_joined_channel,user_left_channel - DMs —
createDM,sendDM,getDMConversations→dm_created,dm_received - Notifications —
subscribeNotifications,getNotifications,markNotificationRead,clearNotifications→notification,notification_read,notifications_cleared - Search —
searchMessages,searchInChannel,searchByUser,filterMessages→ (future results)
For any worker event not wrapped above, subscribe with the raw
client.on('<event>', handler) API — all enhanced broadcasts are
forwarded onto the client surface.
Challenges & Leaderboards
Challenges, leaderboards and achievements build on the same live socket. The
send side lives on the enhanced surface (client.enhanced);
request/query methods resolve with the worker's reply, while progress and
achievement calls are fire-and-forget. Inbound broadcasts arrive on the client
event surface — subscribe with the client's normal
client.on('<event>', handler).
// Receive-path: live challenge broadcasts
client.on('leaderboard_rank_change', (data) => print('rank now ${data['rank']}'));
// Send-path: create, report progress, read standings, finalize
await client.enhanced.createChallenge({'challengeId': 'daily-500', 'metric': 'score', 'ranked': true});
client.enhanced.reportProgress({'challengeId': 'daily-500', 'value': 120});
final board = await client.enhanced.getStandings({'challengeId': 'daily-500', 'limit': 10});
print('top: ${board['standings']}, you: ${board['yourRank']}');
await client.enhanced.completeChallenge({'challengeId': 'daily-500', 'outcome': 'completed'});
Methods
Query and lifecycle methods (createChallenge,
completeChallenge, getStandings,
getAchievements, and the invite request methods) return a
Future<Map<String, dynamic>> that completes with the worker
ack — await them. Progress and achievement calls are
fire-and-forget (no ack).
- createChallenge — create a challenge/leaderboard. Ack
challenge_create_success. - reportProgress — fire-and-forget metric progress. (no ack)
- completeChallenge — finalize with an
outcome. Ackchallenge_complete_success. - unlockAchievement — fire-and-forget; pass
percentComplete(0–100). (no ack) - getStandings — request top-N + caller rank. Ack
challenge_standings_success. - getAchievements — query achievement state. Ack
achievement_state. - sendChallengeInvite — directed invite to another user. Ack
challenge_invite_success. - replyChallengeInvite — accept/decline an invite. Ack
challenge_reply_success. - cancelChallengeInvite — cancel a sent invite. Ack
challenge_invite_cancel_success. - getChallengeInvites — list pending invites. Ack
challenge_invites.
Completion outcomes
Pass one of these as completeChallenge's outcome:
completed— win (rank 1)failed— losstied— drawconceded— resign / concedeexpired— timed out
Progressive achievements
unlockAchievement always emits the wire event
achievement_unlock; the worker is authoritative and derives the
outbound broadcast from percentComplete: a value < 100
broadcasts achievement_progress (status in_progress),
while >= 100 or an omitted value broadcasts
achievement_unlock (status unlocked). You never emit
achievement_progress yourself.
Inbound events
Subscribe to these via the client's client.on(...) API.
- Room broadcasts —
challenge_progress,leaderboard_rank_change,challenge_complete,achievement_unlock,achievement_progress - Directed (per-user) —
challenge_invited,challenge_reply_received,challenge_invite_cancelled
Usage Analytics
Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. getUsageStats() resolves the same four tiles the developer dashboard renders.
final stats = await client.getUsageStats();
// Each tile is nullable: null means "not live yet", never a real zero.
String dash(num? v) => v?.toString() ?? '\u2014';
print('MAU: ${dash(stats.mau)}');
print('DAU: ${dash(stats.dau)}');
print('Messages: ${dash(stats.totalMessages)}');
print('Errors: ${dash(stats.errorRate)}'); // em-dash when null
The four tiles
- mau — monthly active users for your owner scope
- dau — daily active users
- totalMessages — total messages published
- errorRate — publish error rate,
0–1
Honesty rule — null, never a fake zero
Each tile is a number or null. A null means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns null verbatim and never coerces it to 0. Render an em-dash (—) for a null tile so you never show a fabricated zero.
Requires an API key
getUsageStats() reads your owner-scoped analytics, so it needs an apiKey. Keyless / token-only clients have no owner scope to query and will throw getUsageStats requires an apiKey.
Performance & Compatibility
OddSockets Flutter SDK delivers superior performance with broad platform compatibility:
Flutter Support
- Flutter 3.0+ (2022)
- Dart 2.17+ (2022)
- Null Safety
- Sound Type System
Platform Support
- iOS 11+ / Android 21+
- Web (Chrome, Firefox, Safari)
- Windows 10+ / macOS 10.14+
- Linux (Ubuntu 18.04+)
Platform Integrations
The OddSockets Flutter SDK works seamlessly across all Flutter-supported platforms. Here are examples showing platform-specific optimizations:
Mobile (iOS/Android)
class MobileChatApp extends StatefulWidget {
@override
_MobileChatAppState createState() => _MobileChatAppState();
}
class _MobileChatAppState extends State<MobileChatApp> with WidgetsBindingObserver {
late OddSocketsClient client;
late OddSocketsChannel channel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// Mobile-optimized configuration
final config = OddSocketsConfig.builder('ak_live_1234567890abcdef')
.mobile() // Optimizes for battery life and mobile networks
.build();
client = OddSocketsClient(config);
channel = client.channel('mobile-chat');
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
// Handle app lifecycle for better battery management
switch (state) {
case AppLifecycleState.paused:
// Reduce heartbeat frequency when app is backgrounded
break;
case AppLifecycleState.resumed:
// Restore normal operation when app is foregrounded
break;
default:
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Mobile Chat')),
body: StreamBuilder<Message>(
stream: channel.messageStream,
builder: (context, snapshot) {
// Your chat UI here
return Container();
},
),
);
}
}
Web
import 'dart:html' as html;
class WebChatApp extends StatefulWidget {
@override
_WebChatAppState createState() => _WebChatAppState();
}
class _WebChatAppState extends State<WebChatApp> {
late OddSocketsClient client;
late OddSocketsChannel channel;
@override
void initState() {
super.initState();
// Web-optimized configuration
final config = OddSocketsConfig.builder('ak_live_1234567890abcdef')
.web() // Optimizes for web browsers
.build();
client = OddSocketsClient(config);
channel = client.channel('web-chat');
// Handle browser tab visibility for better performance
html.document.addEventListener('visibilitychange', (event) {
if (html.document.hidden ?? false) {
// Reduce activity when tab is not visible
} else {
// Resume normal activity when tab becomes visible
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Web Chat')),
body: StreamBuilder<Message>(
stream: channel.messageStream,
builder: (context, snapshot) {
// Your web chat UI here
return Container();
},
),
),
);
}
}
Desktop (Windows/macOS/Linux)
class DesktopChatApp extends StatefulWidget {
@override
_DesktopChatAppState createState() => _DesktopChatAppState();
}
class _DesktopChatAppState extends State<DesktopChatApp> {
late OddSocketsClient client;
late OddSocketsChannel channel;
@override
void initState() {
super.initState();
// Desktop-optimized configuration
final config = OddSocketsConfig.builder('ak_live_1234567890abcdef')
.desktop() // Optimizes for desktop environments
.build();
client = OddSocketsClient(config);
channel = client.channel('desktop-chat');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Desktop Chat')),
body: Row(
children: [
// Sidebar for desktop layout
Container(
width: 250,
child: StreamBuilder<PresenceInfo?>(
stream: channel.presenceStream,
builder: (context, snapshot) {
// User list sidebar
return Container();
},
),
),
// Main chat area
Expanded(
child: StreamBuilder<Message>(
stream: channel.messageStream,
builder: (context, snapshot) {
// Your desktop chat UI here
return Container();
},
),
),
],
),
),
);
}
}
BLoC Integration
// Events
abstract class ChatEvent {}
class ChatStarted extends ChatEvent {}
class MessageReceived extends ChatEvent {
final Message message;
MessageReceived(this.message);
}
class MessageSent extends ChatEvent {
final String content;
MessageSent(this.content);
}
// States
abstract class ChatState {}
class ChatInitial extends ChatState {}
class ChatConnected extends ChatState {
final List<Message> messages;
ChatConnected(this.messages);
}
// BLoC
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final OddSocketsClient _client;
late final OddSocketsChannel _channel;
late StreamSubscription _messageSubscription;
ChatBloc(this._client) : super(ChatInitial()) {
_channel = _client.channel('chat-room');
on<ChatStarted>(_onChatStarted);
on<MessageReceived>(_onMessageReceived);
on<MessageSent>(_onMessageSent);
}
Future<void> _onChatStarted(ChatStarted event, Emitter<ChatState> emit) async {
await _channel.subscribe((message) {
add(MessageReceived(message));
});
emit(ChatConnected([]));
}
void _onMessageReceived(MessageReceived event, Emitter<ChatState> emit) {
if (state is ChatConnected) {
final currentState = state as ChatConnected;
emit(ChatConnected([...currentState.messages, event.message]));
}
}
Future<void> _onMessageSent(MessageSent event, Emitter<ChatState> emit) async {
await _channel.publish(event.content);
}
@override
Future<void> close() {
_messageSubscription.cancel();
_client.dispose();
return super.close();
}
}