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

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.

  • TypingstartTyping, stopTypinguser_typing, user_stopped_typing
  • ReactionsaddReaction, removeReaction, getReactionsreaction_added, reaction_removed
  • ThreadsthreadReply, getThread, subscribeThread, followThread, markThreadReadthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmarkRead, markAllRead, getUnreadCountsuser_read, unread_count_updated, all_marked_read
  • MessageseditMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessages, searchMessagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statussetStatus, setCustomStatus, setDND, getUserPresenceuser_status_changed, custom_status_updated, dnd_status_changed
  • ChannelscreateChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannelchannel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMscreateDM, sendDM, getDMConversationsdm_created, dm_received
  • NotificationssubscribeNotifications, getNotifications, markNotificationRead, clearNotificationsnotification, notification_read, notifications_cleared
  • SearchsearchMessages, 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 broadcastschallenge_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, 01

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:

<50ms
Latency
99.9%
Uptime
32KB
Max Message
1M+
Messages/sec

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