OddSockets Kotlin SDK
Official Kotlin SDK for OddSockets real-time messaging platform
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
dependencies {
implementation("com.oddsockets:oddsockets-kotlin-sdk:0.1.0-beta.1")
}
dependencies {
implementation 'com.oddsockets:oddsockets-kotlin-sdk:0.1.0-beta.1'
}
<dependency>
<groupId>com.oddsockets</groupId>
<artifactId>oddsockets-kotlin-sdk</artifactId>
<version>0.1.0-beta.1</version>
</dependency>
Quick Start
Basic Usage
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
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
// 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
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
// 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.
- Typing —
startTyping,stopTyping→user_typing,user_stopped_typing - Reactions —
addReaction,removeReaction,getReactions→reaction_added,reaction_removed - Threads —
threadReply,getThread,subscribeThread,followThread,unfollowThread,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→message_edited,message_deleted,message_pinned,message_unpinned - Presence & status —
setStatus,setCustomStatus,clearCustomStatus,setDND,clearDND,getUserPresence→user_status_changed,custom_status_updated,dnd_status_changed - Channels —
createChannel,updateChannel,archiveChannel,inviteToChannel,joinChannel,leaveChannel,getChannelMembers→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→ results returned asJsonObject
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— losstied— drawconceded— resign / concedeexpired— 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 broadcasts —
challenge_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:
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
@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
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
@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
@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)
}
}