tests.ws

Kotlin and Android WebSocket Guide

kotlin android websocket okhttp ktor mobile

Kotlin WebSocket implementations on Android require careful consideration of mobile-specific challenges like lifecycle management, battery optimization, and network reliability. This guide covers building production-ready WebSocket clients using OkHttp and Ktor, the two most popular libraries for Android WebSocket development.

Android WebSocket connections face unique constraints compared to web or server environments. Mobile apps transition between foreground and background states, network connections switch between WiFi and cellular, and the system aggressively manages battery life. Understanding these constraints is essential for building reliable real-time features.

Understanding WebSocket on Mobile

WebSocket provides full-duplex communication over a single TCP connection, making it ideal for chat applications, live updates, and real-time collaboration features. Before implementing WebSocket on Android, familiarize yourself with the WebSocket protocol fundamentals to understand connection establishment, message framing, and closure semantics.

Mobile WebSocket implementations must handle:

  • Activity and fragment lifecycle events
  • Network transitions and interruptions
  • Background execution limits
  • Battery optimization constraints
  • Memory pressure and process death

OkHttp WebSocket Client

OkHttp provides a robust, battle-tested WebSocket implementation that integrates seamlessly with Android. It handles connection management, automatic ping/pong frames, and message queuing.

Basic OkHttp WebSocket Setup

import okhttp3.*
import okio.ByteString

class WebSocketClient {
    private var webSocket: WebSocket? = null
    private val client = OkHttpClient.Builder()
        .pingInterval(30, TimeUnit.SECONDS)
        .build()

    fun connect(url: String) {
        val request = Request.Builder()
            .url(url)
            .build()

        webSocket = client.newWebSocket(request, createListener())
    }

    private fun createListener() = object : WebSocketListener() {
        override fun onOpen(webSocket: WebSocket, response: Response) {
            println("WebSocket connected: ${response.message}")
        }

        override fun onMessage(webSocket: WebSocket, text: String) {
            println("Received text: $text")
            handleMessage(text)
        }

        override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
            println("Received bytes: ${bytes.hex()}")
            handleBinaryMessage(bytes)
        }

        override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
            webSocket.close(1000, null)
            println("WebSocket closing: $code / $reason")
        }

        override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
            println("WebSocket closed: $code / $reason")
        }

        override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
            println("WebSocket error: ${t.message}")
            handleError(t)
        }
    }

    fun sendMessage(message: String) {
        webSocket?.send(message)
    }

    fun disconnect() {
        webSocket?.close(1000, "Client closing")
        client.dispatcher.executorService.shutdown()
    }

    private fun handleMessage(text: String) {
        // Parse and process message
    }

    private fun handleBinaryMessage(bytes: ByteString) {
        // Process binary data
    }

    private fun handleError(throwable: Throwable) {
        // Handle connection errors
    }
}

Advanced OkHttp Configuration

Production applications require custom timeout configurations, interceptors for authentication, and certificate pinning:

import okhttp3.CertificatePinner
import java.util.concurrent.TimeUnit

class SecureWebSocketClient {
    private val certificatePinner = CertificatePinner.Builder()
        .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        .build()

    private val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .pingInterval(30, TimeUnit.SECONDS)
        .certificatePinner(certificatePinner)
        .addInterceptor { chain ->
            val original = chain.request()
            val authorized = original.newBuilder()
                .addHeader("Authorization", "Bearer $accessToken")
                .build()
            chain.proceed(authorized)
        }
        .build()

    private var accessToken: String = ""

    fun setAccessToken(token: String) {
        accessToken = token
    }
}

Ktor WebSocket Client

Ktor provides a coroutine-based WebSocket client that integrates naturally with Kotlin’s structured concurrency model. This approach simplifies state management and cancellation.

Basic Ktor WebSocket Implementation

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class KtorWebSocketClient {
    private val client = HttpClient(CIO) {
        install(WebSockets) {
            pingInterval = 30_000
            maxFrameSize = Long.MAX_VALUE
        }
    }

    private var session: DefaultClientWebSocketSession? = null
    private val messageFlow = MutableSharedFlow<String>()

    suspend fun connect(url: String) {
        client.webSocket(url) {
            session = this

            // Send initial message
            send("Hello from Android")

            // Handle incoming messages
            try {
                for (frame in incoming) {
                    when (frame) {
                        is Frame.Text -> {
                            val message = frame.readText()
                            messageFlow.emit(message)
                        }
                        is Frame.Binary -> {
                            val data = frame.readBytes()
                            handleBinaryData(data)
                        }
                        is Frame.Close -> {
                            val code = frame.readReason()?.code
                            println("Connection closed: $code")
                        }
                        else -> {}
                    }
                }
            } catch (e: Exception) {
                println("Error: ${e.message}")
            }
        }
    }

    suspend fun sendMessage(message: String) {
        session?.send(Frame.Text(message))
    }

    fun observeMessages(): Flow<String> = messageFlow.asSharedFlow()

    suspend fun disconnect() {
        session?.close(CloseReason(CloseReason.Codes.NORMAL, "Client closing"))
        client.close()
    }

    private fun handleBinaryData(data: ByteArray) {
        // Process binary frames
    }
}

Ktor with Flow-Based Architecture

Kotlin Flow provides reactive state management for WebSocket events:

class FlowBasedWebSocketClient {
    private val client = HttpClient(CIO) {
        install(WebSockets)
    }

    sealed class WebSocketEvent {
        data class Connected(val url: String) : WebSocketEvent()
        data class MessageReceived(val message: String) : WebSocketEvent()
        data class Error(val exception: Throwable) : WebSocketEvent()
        object Disconnected : WebSocketEvent()
    }

    fun connectAndObserve(url: String): Flow<WebSocketEvent> = flow {
        try {
            client.webSocket(url) {
                emit(WebSocketEvent.Connected(url))

                for (frame in incoming) {
                    when (frame) {
                        is Frame.Text -> {
                            emit(WebSocketEvent.MessageReceived(frame.readText()))
                        }
                        is Frame.Close -> {
                            emit(WebSocketEvent.Disconnected)
                            break
                        }
                        else -> {}
                    }
                }
            }
        } catch (e: Exception) {
            emit(WebSocketEvent.Error(e))
        }
    }.flowOn(Dispatchers.IO)
}

Android Lifecycle Handling

Android lifecycle events require explicit WebSocket management to prevent memory leaks and unnecessary battery drain.

ViewModel Integration

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class ChatViewModel : ViewModel() {
    private val webSocketClient = KtorWebSocketClient()
    private val _messages = MutableStateFlow<List<String>>(emptyList())
    val messages: StateFlow<List<String>> = _messages.asStateFlow()

    fun connect() {
        viewModelScope.launch {
            webSocketClient.connect("wss://api.example.com/chat")
            webSocketClient.observeMessages().collect { message ->
                _messages.value = _messages.value + message
            }
        }
    }

    fun sendMessage(message: String) {
        viewModelScope.launch {
            webSocketClient.sendMessage(message)
        }
    }

    override fun onCleared() {
        super.onCleared()
        viewModelScope.launch {
            webSocketClient.disconnect()
        }
    }
}

Activity Lifecycle Management

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class ChatActivity : AppCompatActivity() {
    private lateinit var webSocketClient: WebSocketClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        webSocketClient = WebSocketClient()
    }

    override fun onStart() {
        super.onStart()
        webSocketClient.connect("wss://api.example.com/chat")
    }

    override fun onStop() {
        super.onStop()
        if (isFinishing) {
            webSocketClient.disconnect()
        }
    }
}

Background WebSocket Operations

Android restricts background execution to preserve battery life. Long-running WebSocket connections require either WorkManager for periodic updates or a foreground service for continuous connections.

Foreground Service for Persistent Connections

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.*

class WebSocketService : Service() {
    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private lateinit var webSocketClient: KtorWebSocketClient

    override fun onCreate() {
        super.onCreate()
        webSocketClient = KtorWebSocketClient()
        createNotificationChannel()
        startForeground(NOTIFICATION_ID, createNotification())
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            webSocketClient.connect("wss://api.example.com/notifications")
            webSocketClient.observeMessages().collect { message ->
                handleBackgroundMessage(message)
            }
        }
        return START_STICKY
    }

    private fun handleBackgroundMessage(message: String) {
        // Show notification or update app state
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "WebSocket Service",
                NotificationManager.IMPORTANCE_LOW
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }

    private fun createNotification() = NotificationCompat.Builder(this, CHANNEL_ID)
        .setContentTitle("Connected to server")
        .setContentText("Receiving real-time updates")
        .setSmallIcon(R.drawable.ic_notification)
        .build()

    override fun onDestroy() {
        serviceScope.launch {
            webSocketClient.disconnect()
        }
        serviceScope.cancel()
        super.onDestroy()
    }

    override fun onBind(intent: Intent?): IBinder? = null

    companion object {
        private const val CHANNEL_ID = "websocket_service_channel"
        private const val NOTIFICATION_ID = 1
    }
}

WorkManager for Periodic WebSocket Checks

import androidx.work.*
import java.util.concurrent.TimeUnit

class WebSocketWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return try {
            val client = KtorWebSocketClient()
            client.connect("wss://api.example.com/sync")
            client.sendMessage("fetch_updates")

            // Wait for response
            delay(5000)

            client.disconnect()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }

    companion object {
        fun schedulePeriodicWork(context: Context) {
            val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

            val workRequest = PeriodicWorkRequestBuilder<WebSocketWorker>(
                15, TimeUnit.MINUTES
            )
                .setConstraints(constraints)
                .build()

            WorkManager.getInstance(context)
                .enqueueUniquePeriodicWork(
                    "websocket_sync",
                    ExistingPeriodicWorkPolicy.KEEP,
                    workRequest
                )
        }
    }
}

Reconnection Strategies

Network interruptions require automatic reconnection with exponential backoff to avoid overwhelming the server.

Exponential Backoff Reconnection

import kotlinx.coroutines.delay
import kotlin.math.pow

class ReconnectingWebSocketClient {
    private val client = KtorWebSocketClient()
    private var reconnectAttempt = 0
    private val maxReconnectAttempts = 5
    private val baseDelay = 1000L

    suspend fun connectWithRetry(url: String) {
        while (reconnectAttempt < maxReconnectAttempts) {
            try {
                client.connect(url)
                reconnectAttempt = 0 // Reset on success
                break
            } catch (e: Exception) {
                reconnectAttempt++
                if (reconnectAttempt >= maxReconnectAttempts) {
                    throw Exception("Max reconnection attempts reached")
                }

                val delayMs = baseDelay * 2.0.pow(reconnectAttempt).toLong()
                println("Reconnecting in ${delayMs}ms (attempt $reconnectAttempt)")
                delay(delayMs)
            }
        }
    }
}

Network-Aware Reconnection

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest

class NetworkAwareWebSocketClient(private val context: Context) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private val webSocketClient = KtorWebSocketClient()

    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            CoroutineScope(Dispatchers.IO).launch {
                reconnect()
            }
        }

        override fun onLost(network: Network) {
            CoroutineScope(Dispatchers.IO).launch {
                webSocketClient.disconnect()
            }
        }
    }

    fun startNetworkMonitoring() {
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        connectivityManager.registerNetworkCallback(request, networkCallback)
    }

    private suspend fun reconnect() {
        webSocketClient.connect("wss://api.example.com/chat")
    }

    fun stopNetworkMonitoring() {
        connectivityManager.unregisterNetworkCallback(networkCallback)
    }
}

Kotlin Coroutines and WebSocket

Kotlin coroutines provide structured concurrency for managing WebSocket connections and message processing.

Channel-Based Message Processing

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

class ChannelBasedWebSocketClient {
    private val outgoingMessages = Channel<String>(Channel.UNLIMITED)
    private val incomingMessages = Channel<String>(Channel.UNLIMITED)

    suspend fun start(url: String) = coroutineScope {
        val client = HttpClient(CIO) {
            install(WebSockets)
        }

        client.webSocket(url) {
            // Launch sender coroutine
            launch {
                for (message in outgoingMessages) {
                    send(Frame.Text(message))
                }
            }

            // Launch receiver coroutine
            launch {
                for (frame in incoming) {
                    if (frame is Frame.Text) {
                        incomingMessages.send(frame.readText())
                    }
                }
            }
        }
    }

    suspend fun send(message: String) {
        outgoingMessages.send(message)
    }

    suspend fun receive(): String {
        return incomingMessages.receive()
    }
}

Testing WebSocket Connections

Proper testing ensures reliability across network conditions and edge cases. Learn more about WebSocket testing strategies.

Mock WebSocket Server for Testing

import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlinx.coroutines.runBlocking

class WebSocketClientTest {
    private lateinit var mockServer: MockWebServer
    private lateinit var webSocketClient: WebSocketClient

    @Before
    fun setup() {
        mockServer = MockWebServer()
        mockServer.start()
        webSocketClient = WebSocketClient()
    }

    @Test
    fun testWebSocketConnection() = runBlocking {
        val url = mockServer.url("/").toString().replace("http", "ws")

        mockServer.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {
            override fun onMessage(webSocket: WebSocket, text: String) {
                webSocket.send("Echo: $text")
            }
        }))

        webSocketClient.connect(url)
        webSocketClient.sendMessage("Hello")

        // Assert message received
    }

    @After
    fun teardown() {
        mockServer.shutdown()
    }
}

Integration Testing with Real Servers

import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class WebSocketIntegrationTest {
    @Test
    fun testRealServerConnection() = runBlocking {
        val client = KtorWebSocketClient()

        try {
            client.connect("wss://echo.websocket.org")
            client.sendMessage("Test message")

            // Collect messages with timeout
            val received = withTimeout(5000) {
                client.observeMessages().first()
            }

            assert(received == "Test message")
        } finally {
            client.disconnect()
        }
    }
}

Handling WebSocket Close Codes

Understanding WebSocket close codes helps implement proper error handling and reconnection logic.

import io.ktor.websocket.CloseReason

fun handleCloseCode(reason: CloseReason?) {
    when (reason?.code) {
        1000 -> {
            // Normal closure, no reconnection needed
            println("Connection closed normally")
        }
        1001 -> {
            // Going away (server shutdown)
            scheduleReconnection()
        }
        1006 -> {
            // Abnormal closure (connection lost)
            reconnectImmediately()
        }
        1008 -> {
            // Policy violation
            notifyUser("Connection terminated due to policy violation")
        }
        1011 -> {
            // Server error
            scheduleReconnectionWithBackoff()
        }
        else -> {
            println("Unexpected close code: ${reason?.code}")
        }
    }
}

private fun scheduleReconnection() {
    // Implement reconnection logic
}

private fun reconnectImmediately() {
    // Immediate reconnection
}

private fun notifyUser(message: String) {
    // Show user notification
}

private fun scheduleReconnectionWithBackoff() {
    // Exponential backoff reconnection
}

Frequently Asked Questions

Should I use OkHttp or Ktor for Android WebSocket development?

Choose OkHttp for simpler projects with callback-based architecture or when you’re already using OkHttp for HTTP requests. Choose Ktor when building coroutine-based applications, when you need Flow integration, or when working with Kotlin Multiplatform projects. Ktor provides better integration with modern Kotlin idioms, while OkHttp offers a more traditional callback approach with wider ecosystem support.

How do I keep WebSocket connections alive during Android Doze mode?

Android Doze mode severely restricts background network access. For critical real-time features, implement a foreground service with an ongoing notification. For less critical features, use Firebase Cloud Messaging for push notifications when the app is in Doze mode, then establish a WebSocket connection when the user opens the app. WorkManager’s periodic tasks won’t execute during Doze windows, so they’re unsuitable for real-time requirements.

What’s the best way to handle WebSocket authentication on Android?

Pass authentication tokens in the initial HTTP upgrade request headers rather than sending them as the first WebSocket message. This prevents establishing connections that will immediately be rejected. Use OkHttp interceptors or Ktor’s client configuration to add Authorization headers. Implement token refresh logic that closes and reconnects the WebSocket when tokens expire, using refresh tokens to obtain new access tokens before reconnection.

How do I test WebSocket connections that require network connectivity?

Use MockWebServer from OkHttp for unit testing WebSocket logic without network access. For integration tests, use Echo WebSocket servers like wss://echo.websocket.org or deploy a test server. Mock network conditions using Android Emulator controls to simulate poor connectivity, network switches, and disconnections. Test reconnection logic by toggling airplane mode and switching between WiFi and cellular networks during active connections.