Kotlin and Android WebSocket Guide
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.