OddSockets Flutter SDK

Official Flutter/Dart SDK for OddSockets real-time messaging platform

Flutter Ready Dart/Flutter Cross-platform High Performance Stream-based

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

Install from source for now. The pub.dev package is not published yet, so the commands in the tabs below will not resolve. Use this instead — it pulls the same code the tabs will install once the package is live:
yaml
# pubspec.yaml
dependencies:
  oddsockets_flutter:
    git:
      url: https://github.com/jyswee/oddsockets-flutter-sdk
yaml
dependencies:
  oddsockets_flutter: ^1.0.0
bash
flutter pub add oddsockets_flutter
bash
dart pub add oddsockets_flutter

Quick Start

Basic Usage

dart
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

dart
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

dart
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

dart
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

dart
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

dart
// 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

dart
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).

dart
// 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. Ack challenge_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 — loss
  • tied — draw
  • conceded — resign / concede
  • expired — 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

Platform limits and plan ceilings, plus the versions and platforms this SDK supports:

32KB
Max message size
100M
Messages/mo (Scale)
5,000
Peak connections (Scale)
99.999%
Uptime SLA (Enterprise)

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)

dart
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

dart
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)

dart
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

dart
// 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();
  }
}