OddSockets Kotlin SDK

Official Kotlin SDK for OddSockets real-time messaging platform

Android Ready JVM Compatible Coroutines High Performance Type Safe

Overview & Features

The OddSockets Kotlin SDK provides a powerful, coroutine-based interface for real-time messaging in Android applications and JVM environments.

Android Native

Built specifically for Android with lifecycle-aware components and modern architecture patterns.

Kotlin Coroutines

Fully async with Kotlin coroutines and Flow for reactive programming patterns.

Type Safety

Complete type safety with Kotlin's null safety and comprehensive error handling.

High Performance

Optimized for mobile with efficient WebSocket connections and smart resource management.

Cost Effective

No per-message pricing, industry-standard 32KB message limits, transparent pricing.

Session Stickiness

Built-in session management for consistent worker assignment and optimal performance.

Installation

kotlin
dependencies {
    implementation("com.oddsockets:oddsockets-kotlin-sdk:0.1.0-beta.1")
}
groovy
dependencies {
    implementation 'com.oddsockets:oddsockets-kotlin-sdk:0.1.0-beta.1'
}
xml
<dependency>
    <groupId>com.oddsockets</groupId>
    <artifactId>oddsockets-kotlin-sdk</artifactId>
    <version>0.1.0-beta.1</version>
</dependency>

Quick Start

Basic Usage

kotlin
import com.oddsockets.OddSocketsClient
import com.oddsockets.config.OddSocketsConfig
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val client = OddSocketsClient(
        OddSocketsConfig.default("ak_live_1234567890abcdef")
    )

    val channel = client.channel("my-channel")

    // Subscribe to messages
    channel.subscribe { message ->
        println("Received: ${message.message}")
    }

    // Publish a message
    channel.publish("Hello, World!")
}

Android Usage

kotlin
class MainActivity : AppCompatActivity() {
    private lateinit var client: OddSocketsClient
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        client = OddSocketsClient(
            OddSocketsConfig.default("ak_live_1234567890abcdef")
        )
        
        val channel = client.channel("chat-room")
        
        // Subscribe with lifecycle awareness
        lifecycleScope.launch {
            channel.messageFlow.collect { message ->
                // Update UI with new message
                updateChatUI(message)
            }
        }
        
        // Publish message
        lifecycleScope.launch {
            channel.publish("Hello from Android!")
        }
    }
    
    override fun onDestroy() {
        super.onDestroy()
        client.close()
    }
}

Coroutines & Flow

kotlin
// Using Flow for reactive programming
channel.messageFlow
    .filter { it.metadata?.get("priority") == "high" }
    .map { it.message.toString() }
    .collect { highPriorityMessage ->
        handleUrgentMessage(highPriorityMessage)
    }

// Connection state monitoring
client.connectionState
    .collect { state ->
        when (state) {
            ConnectionState.CONNECTED -> showConnectedUI()
            ConnectionState.DISCONNECTED -> showDisconnectedUI()
            ConnectionState.RECONNECTING -> showReconnectingUI()
        }
    }

Configuration

Client Configuration

kotlin
val client = OddSocketsClient.create("ak_live_1234567890abcdef") {
    userId = "user-123"                    // Optional: User identifier
    autoConnect = true                     // Optional: Auto-connect on creation
    reconnectAttempts = 5                  // Optional: Max reconnection attempts
    heartbeatInterval = 30.seconds         // Optional: Heartbeat interval
    timeout = 10.seconds                   // Optional: Operation timeout
}

Channel Configuration

kotlin
// Subscribe with options
channel.subscribe(messageHandler) {
    enablePresence = true                  // Enable presence tracking
    retainHistory = true                   // Retain message history
    filterExpression = "user.premium == true"  // Message filter
}

// Publish with options
channel.publish("Hello World!") {
    ttl = 3600                            // Time to live (seconds)
    metadata = mapOf("priority" to "high") // Additional metadata
    storeInHistory = true                 // Store in message history
}

Examples

Explore comprehensive examples demonstrating the OddSockets Kotlin 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. Send an action with a client.enhanced.* method (camelCase, positional arguments); receive the paired broadcast with client.on("<event>") { data -> ... }. The worker forwards every enhanced broadcast onto the client's raw event surface, delivered as a kotlinx.serialization.json.JsonElement?.

Typing & Reactions

val channel = client.channel("room-42")
channel.subscribe { msg -> println("Received: $msg") }

// Receive-path: broadcasts from other users on the channel
client.on("user_typing")    { data -> println("someone is typing: $data") }
client.on("reaction_added") { data -> println("reaction added: $data") }

// Send-path: enhanced actions over the live socket
client.enhanced.startTyping("alice", "room-42")
client.enhanced.addReaction("msg-1", "room-42", ":thumbsup:", "alice", "Alice")

Threads

client.on("thread_reply") { data -> println("new thread reply: $data") }

// suspend request-style methods return a JsonObject with the worker ack
val reply = client.enhanced.threadReply(
    "room-42", "msg-1", "Replying in the thread", "alice", "Alice")

val thread = client.enhanced.getThread("thread-123")

Each area exposes methods on client.enhanced; the worker broadcasts the paired events which you handle with client.on(...). Query methods (get*, search*) and the request-style actions are suspend functions that return a JsonObject with the worker response.

  • TypingstartTyping, stopTypinguser_typing, user_stopped_typing
  • ReactionsaddReaction, removeReaction, getReactionsreaction_added, reaction_removed
  • ThreadsthreadReply, getThread, subscribeThread, followThread, unfollowThread, markThreadReadthread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receiptsmarkRead, markAllRead, getUnreadCountsuser_read, unread_count_updated, all_marked_read
  • MessageseditMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessagesmessage_edited, message_deleted, message_pinned, message_unpinned
  • Presence & statussetStatus, setCustomStatus, clearCustomStatus, setDND, clearDND, getUserPresenceuser_status_changed, custom_status_updated, dnd_status_changed
  • ChannelscreateChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannel, getChannelMemberschannel_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 → results returned as JsonObject

For any worker event not wrapped above, subscribe with the raw client.on("<event>") { ... } 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 are suspend functions that 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>") { data -> ... }.

// Receive-path: standings + lifecycle broadcasts on the client surface
client.on("challenge_progress")      { data -> println("progress: $data") }
client.on("leaderboard_rank_change") { data -> println("rank change: $data") }

// Send-path: create, report progress, read standings, finalize
client.enhanced.createChallenge("weekly-sprint", "points", ranked = true)
client.enhanced.reportProgress("weekly-sprint", 250.0)
val standings = client.enhanced.getStandings("weekly-sprint")   // suspend, awaits ack
client.enhanced.completeChallenge("weekly-sprint", "completed")

Send an action with a client.enhanced.* method (camelCase, positional/named arguments). Request-style methods suspend and return a JsonObject carrying the named worker ack; fire-and-forget methods return immediately with 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 to completeChallenge as the outcome:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — timed out

Progressive achievements

unlockAchievement always emits the single wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete. A value < 100 fans out as achievement_progress (status in_progress); a value >= 100 (or omitted) fans out as achievement_unlock (status unlocked). You never emit achievement_progress yourself.

Inbound events

Subscribe to these with client.on("<event>") { data -> ... }:

  • Room broadcastschallenge_progress, leaderboard_rank_change, challenge_complete, achievement_unlock, achievement_progress
  • Directed (per-user)challenge_invited, challenge_reply_received, challenge_invite_cancelled

Performance & Compatibility

OddSockets Kotlin SDK delivers superior performance with broad compatibility:

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

Platform Support

  • Android API 21+ (5.0 Lollipop)
  • JVM 8+ (Server-side)
  • Kotlin 1.8+
  • Kotlin Multiplatform

Dependencies

  • Kotlin Coroutines
  • Ktor Client
  • Kotlinx Serialization
  • Kotlin Flow

Framework Integrations

The OddSockets Kotlin SDK works seamlessly with modern Android architecture patterns and frameworks:

Jetpack Compose

kotlin
@Composable
fun ChatScreen(client: OddSocketsClient) {
    val messages by client.channel("chat")
        .messageFlow
        .collectAsState(initial = emptyList())
    
    val connectionState by client.connectionState
        .collectAsState()
    
    Column {
        // Connection status
        when (connectionState) {
            ConnectionState.CONNECTED -> {
                Text("Connected", color = Color.Green)
            }
            ConnectionState.RECONNECTING -> {
                Text("Reconnecting...", color = Color.Orange)
            }
            else -> {
                Text("Disconnected", color = Color.Red)
            }
        }
        
        // Messages list
        LazyColumn {
            items(messages) { message ->
                MessageItem(message = message)
            }
        }
        
        // Send message
        var inputText by remember { mutableStateOf("") }
        Row {
            TextField(
                value = inputText,
                onValueChange = { inputText = it }
            )
            Button(
                onClick = {
                    client.channel("chat").publish(inputText)
                    inputText = ""
                }
            ) {
                Text("Send")
            }
        }
    }
}

MVVM with ViewModel

kotlin
class ChatViewModel : ViewModel() {
    private val client = OddSocketsClient(
        OddSocketsConfig.default("ak_live_1234567890abcdef")
    )
    
    private val channel = client.channel("chat-room")
    
    private val _messages = MutableLiveData<List<Message>>()
    val messages: LiveData<List<Message>> = _messages
    
    private val _connectionState = MutableLiveData<ConnectionState>()
    val connectionState: LiveData<ConnectionState> = _connectionState
    
    init {
        // Observe connection state
        viewModelScope.launch {
            client.connectionState.collect { state ->
                _connectionState.value = state
            }
        }
        
        // Subscribe to messages
        viewModelScope.launch {
            channel.subscribe { message ->
                val currentMessages = _messages.value ?: emptyList()
                _messages.value = currentMessages + message
            }
        }
    }
    
    fun sendMessage(text: String) {
        viewModelScope.launch {
            try {
                channel.publish(text)
            } catch (e: OddSocketsException) {
                // Handle error
                Log.e("ChatViewModel", "Failed to send message", e)
            }
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        client.close()
    }
}

Dagger Hilt Integration

kotlin
@Module
@InstallIn(SingletonComponent::class)
object OddSocketsModule {
    
    @Provides
    @Singleton
    fun provideOddSocketsClient(): OddSocketsClient {
        return OddSocketsClient(
            OddSocketsConfig.default("ak_live_1234567890abcdef")
        )
    }
}

@HiltViewModel
class ChatViewModel @Inject constructor(
    private val oddSocketsClient: OddSocketsClient
) : ViewModel() {
    
    private val channel = oddSocketsClient.channel("chat")
    
    val messages = channel.messageFlow
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
    
    fun sendMessage(text: String) {
        viewModelScope.launch {
            channel.publish(text)
        }
    }
}

Room Database Integration

kotlin
@Entity(tableName = "messages")
data class MessageEntity(
    @PrimaryKey val id: String,
    val channel: String,
    val content: String,
    val timestamp: Long,
    val userId: String
)

@Dao
interface MessageDao {
    @Query("SELECT * FROM messages WHERE channel = :channel ORDER BY timestamp ASC")
    fun getMessagesForChannel(channel: String): Flow<List<MessageEntity>>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertMessage(message: MessageEntity)
}

class ChatRepository @Inject constructor(
    private val messageDao: MessageDao,
    private val oddSocketsClient: OddSocketsClient
) {
    
    fun getMessages(channelName: String): Flow<List<MessageEntity>> {
        return messageDao.getMessagesForChannel(channelName)
    }
    
    suspend fun subscribeToChannel(channelName: String) {
        val channel = oddSocketsClient.channel(channelName)
        
        channel.subscribe { message ->
            // Store message in local database
            val entity = MessageEntity(
                id = message.id,
                channel = channelName,
                content = message.message.toString(),
                timestamp = message.timestamp.toEpochMilli(),
                userId = message.userId
            )
            messageDao.insertMessage(entity)
        }
    }
    
    suspend fun sendMessage(channelName: String, content: String) {
        oddSocketsClient.channel(channelName).publish(content)
    }
}