Introduction
Real-time features are table stakes in modern mobile apps—from live notifications and collaborative editing to trading feeds and ride-hailing location updates. WebSockets in Android: a practical Android & Kotlin guide will walk you through building a production-ready WebSocket client that handles connection lifecycle, automatic reconnection, and clean resource management. We'll use OkHttp's battle-tested WebSocket implementation and expose messages through Kotlin Flow for seamless integration with your app's architecture.
Why WebSockets matter in Android
HTTP polling burns battery and data. Server-sent events don't work well for bidirectional streams. WebSockets give you persistent, low-latency two-way communication—essential when your users expect live updates without noticeable delay. On Android, though, network conditions change constantly: users move between WiFi and mobile, apps are backgrounded, and connections drop. A naive WebSocket implementation will fail in production within hours.
OkHttp WebSocket client
OkHttp is the de facto HTTP client for Android. Its WebSocket support is production-grade and integrates neatly with your existing networking stack. Rather than reaching for a third-party library, we start with OkHttp's core WebSocketListener and Request API.
Here's a basic WebSocket listener implementation:
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
class WebSocketManager(private val okHttpClient: OkHttpClient) {
private var webSocket: WebSocket? = null
fun connect(url: String) {
val request = Request.Builder()
.url(url)
.build()
webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) {
println("WebSocket opened")
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("Message received: $text")
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
println("Bytes received: ${bytes.hex()}")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(1000, null)
println("WebSocket closing: $code $reason")
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: okhttp3.Response?
) {
println("WebSocket failure: ${t.message}")
}
})
}
fun send(message: String) {
webSocket?.send(message)
}
fun disconnect() {
webSocket?.close(1000, "Client disconnect")
}
}
This works for a demo, but in production you'll hit three problems immediately: no reconnection logic, no Flow-based message delivery, and no lifecycle awareness. Let's solve each one.
Reconnect with backoff
Network failures are inevitable. A dropped connection won't automatically reconnect—your app goes silent. Exponential backoff prevents hammering the server when it's down and gives it breathing room to recover. We implement this with a coroutine-based retry strategy:
import kotlinx.coroutines.*
import kotlin.math.min
class WebSocketManager(
private val okHttpClient: OkHttpClient,
private val scope: CoroutineScope
) {
private var webSocket: WebSocket? = null
private var reconnectJob: Job? = null
private val maxBackoffMs = 30_000L
private var backoffMs = 1_000L
fun connect(url: String) {
reconnectJob = scope.launch {
while (isActive) {
try {
connectOnce(url)
backoffMs = 1_000L // Reset backoff on success
return@launch
} catch (e: Exception) {
if (!isActive) return@launch
delay(backoffMs)
backoffMs = min(backoffMs * 2, maxBackoffMs)
}
}
}
}
private suspend fun connectOnce(url: String) = suspendCancellableCoroutine { continuation ->
val request = Request.Builder().url(url).build()
webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) {
continuation.resume(Unit)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: okhttp3.Response?
) {
continuation.resumeWithException(t)
}
// Handle onMessage and onClosing...
})
continuation.invokeOnCancellation {
webSocket?.close(1000, null)
}
}
fun disconnect() {
reconnectJob?.cancel()
webSocket?.close(1000, "Client disconnect")
}
}
The key insight: backoffMs doubles on each failure, capped at 30 seconds. When a connection succeeds, we reset to 1 second. This handles transient network glitches (brief retry) while protecting the server during outages (progressively longer delays).
Expose messages as Flow
Coroutine Flow is how we bridge WebSocket callbacks into our app's reactive architecture. Instead of callback hell, messages become a clean stream you can collect, transform, and combine with other streams:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
sealed class WebSocketEvent {
data class Message(val text: String) : WebSocketEvent()
data class Error(val throwable: Throwable) : WebSocketEvent()
object Connected : WebSocketEvent()
object Disconnected : WebSocketEvent()
}
class WebSocketManager(
private val okHttpClient: OkHttpClient,
private val scope: CoroutineScope
) {
private var webSocket: WebSocket? = null
private var reconnectJob: Job? = null
private val _events = MutableSharedFlow(
replay = 0,
extraBufferCapacity = 64
)
val events: Flow = _events
private val maxBackoffMs = 30_000L
private var backoffMs = 1_000L
fun connect(url: String) {
reconnectJob = scope.launch {
while (isActive) {
try {
connectOnce(url)
backoffMs = 1_000L
return@launch
} catch (e: Exception) {
_events.emit(WebSocketEvent.Error(e))
if (!isActive) return@launch
delay(backoffMs)
backoffMs = min(backoffMs * 2, maxBackoffMs)
}
}
}
}
private suspend fun connectOnce(url: String) = suspendCancellableCoroutine { continuation ->
val request = Request.Builder().url(url).build()
webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) {
scope.launch { _events.emit(WebSocketEvent.Connected) }
continuation.resume(Unit)
}
override fun onMessage(webSocket: WebSocket, text: String) {
scope.launch { _events.emit(WebSocketEvent.Message(text)) }
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
scope.launch { _events.emit(WebSocketEvent.Disconnected) }
webSocket.close(1000, null)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: okhttp3.Response?
) {
scope.launch { _events.emit(WebSocketEvent.Error(t)) }
continuation.resumeWithException(t)
}
})
continuation.invokeOnCancellation {
webSocket?.close(1000, null)
}
}
fun send(message: String) {
webSocket?.send(message)
}
fun disconnect() {
reconnectJob?.cancel()
webSocket?.close(1000, "Client disconnect")
}
}
Now your UI can collect and react to events naturally:
// In your ViewModel or composable
viewModelScope.launch {
websocketManager.events.collect { event ->
when (event) {
is WebSocketEvent.Message -> handleMessage(event.text)
is WebSocketEvent.Connected -> updateUI("Connected")
is WebSocketEvent.Error -> showError(event.throwable)
is WebSocketEvent.Disconnected -> updateUI("Disconnected")
}
}
}
Close when the app is backgrounded
Backgrounded apps should not hold WebSocket connections. Battery drain, unnecessary data usage, and server resource waste all result from keeping a connection alive while the user is elsewhere. Lifecycle-aware management ensures we clean up properly:
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
class WebSocketLifecycleObserver(
private val websocketManager: WebSocketManager
) : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
when (event) {
Lifecycle.Event.ON_RESUME -> {
websocketManager.connect("wss://api.example.com/stream")
}
Lifecycle.Event.ON_PAUSE -> {
websocketManager.disconnect()
}
else -> {}
}
}
}
// In your Activity or Fragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(WebSocketLifecycleObserver(websocketManager))
}
This observer connects when your Activity/Fragment comes to the foreground and closes when it goes to the background. For multi-screen apps, track a reference count—only truly close when the last screen backgrounding occurs.
Alternatively, use Jetpack Compose lifecycle:
@Composable
fun MyScreen(viewModel: MyViewModel) {
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> viewModel.connectWebSocket()
Lifecycle.Event.ON_PAUSE -> viewModel.disconnectWebSocket()
else -> {}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
// UI here
}
Common mistakes
- Ignoring onClosing: Always consume and close the WebSocket in
onClosing. Ignoring this callback leaves connections in a half-open state. - Reconnecting without backoff: Immediate reconnection hammers the server and wastes battery. Always implement exponential backoff with a cap.
- Blocking the WebSocket thread: OkHttp calls your callbacks on a background thread. Don't do heavy work there—emit to Flow and handle on the main thread or coroutine scope.
- Leaking scope: If you pass
GlobalScopeto the manager, it will outlive your Activity. Always useviewModelScopeorlifecycleScopeso cancellation propagates correctly. - Keeping connections on backgrounded apps: Drains battery and wastes data. Hook into Lifecycle and close on
ON_PAUSE. - No message ordering guarantees: MutableSharedFlow is unordered across collectors. If order is critical, use a Channel with a specific ordering discipline instead.
Key takeaways
- OkHttp's WebSocket API is production-ready; use it instead of rolling your own or reaching for a bloated third-party library.
- Implement exponential backoff starting at 1 second and capping at 30 seconds to handle transient failures and protect servers during outages.
- Expose WebSocket events through Kotlin Flow via a sealed class (Connected, Disconnected, Message, Error) for clean reactive architecture integration.
- Close WebSocket connections when the app backgrounding (ON_PAUSE) using Lifecycle observers to minimize battery drain and data usage.
- Never block OkHttp's callback thread; always emit to Flow or launch coroutines on your scope to keep networking async.
- Reset backoff on successful connection so transient failures retry quickly but outages back off gracefully.






Comments
Loading comments…