WebSockets for real-time Android apps
WebSockets create a persistent, bidirectional connection between an Android app and a server. The client can send events without opening another HTTP request, while the server can push chat replies, presence changes, scores, or job progress immediately. This low-latency channel is the foundation of real-time communication in Android applications.
A dependable feature needs more than an open socket. Plan the message contract, secure the handshake, connect it to the app lifecycle, and recover cleanly from lost mobile coverage. The examples below use Kotlin, OkHttp, coroutines, and StateFlow, while the transport principles also suit Java and other client libraries.
Choosing a real-time transport
Compare the available mechanisms before writing connection code:
| Mechanism | Traffic direction | Response profile | Best Android use |
|---|---|---|---|
| WebSocket | Bidirectional | Low latency | Chat, collaboration and live dashboards |
| Long polling | Repeated client requests | Medium latency | Legacy systems with limited server support |
| Server-sent events | Server to client | Low latency | Feeds and one-way notifications |
| Periodic polling | Repeated client requests | Highest latency | Infrequent updates and restricted networks |
WebSockets suit two-way events such as messaging, collaborative tools, and live operational dashboards. Long polling is easier on some legacy servers, server-sent events suit one-way feeds, and periodic polling remains useful when infrastructure blocks upgraded connections.
Designing the message contract
Keep every frame small and explicit. A JSON event can contain a type, an event ID, a timestamp, and an optional payload, such as {"type":"presence.updated","id":"evt_42","payload":{"online":true}}. Define allowed event types and payload schemas on both sides because unvalidated JSON becomes difficult to maintain as a feature grows.
Give ordered events a sequence number or cursor so the client can detect gaps and request a replay after reconnecting. Use acknowledgements only for actions requiring confirmation, and version the contract so older Android builds can negotiate with the server. Treat the socket as a live channel rather than a durable message queue.
Connecting with OkHttp
OkHttp exposes WebSockets through WebSocketListener. Build one client for the repository, configure ping intervals, and use a short-lived access token:
val client = OkHttpClient.Builder()
.pingInterval(20, TimeUnit.SECONDS)
.build()
val listener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
// Parse the JSON frame and publish an application event.
}
}
val request = Request.Builder()
.url("wss://api.example.com/live")
.build()
val socket = client.newWebSocket(request, listener)
Callbacks arrive on OkHttp’s dispatcher thread, so parse and validate JSON there before publishing application events to a MutableStateFlow or another lifecycle-aware state holder. Let the UI collect that flow on the main dispatcher. Cancel the socket when its owning scope ends, and prevent callbacks from an older connection from overwriting state created by a newer attempt.
Managing state and user feedback
Keep transport status separate from domain state. A chat screen can remain open while its socket reconnects, and cached messages can remain visible during an outage. Store durable drafts or unacknowledged actions in SQLite when needed, then let a ListView or RecyclerView adapter render the latest flow.
Connection and event checklist
Connection actions
- Open the socket only after obtaining an access token.
- Cancel stale connection attempts when their owner leaves.
- Derive online status instead of storing it manually.
- Reconnect with capped exponential backoff and jitter.
Event actions
- Ignore duplicate sequence identifiers.
- Validate payload size and schema before parsing.
- Dispatch UI updates through the owning coroutine scope.
- Acknowledge only events that require replay or confirmation.
Show a readable status without replacing useful content with a technical error. Australian English such as “No worries—reconnecting” or “Hang on” can suit a casual consumer app when it matches the brand. Preserve the last known messages and offer manual retry when automatic recovery stalls.
Recovering from network changes
Android can change networks while the app runs. Register a ConnectivityManager.NetworkCallback, close sockets tied to the old network, and open a new one after connectivity returns. This matters on Sydney and Melbourne train routes, where tunnels and underground sections can interrupt mobile data for short periods.
Reconnect with capped exponential backoff and random jitter so many clients do not return simultaneously. Do not retry indefinitely in the background because Android may restrict that work, and a foreground service still needs a clear user-visible purpose. Keep Perth and regional Western Australia users in the test matrix, since a backend near Sydney cannot provide identical latency everywhere. If the process dies, restore local state and request events after the server’s replay cursor instead of assuming the old socket survived.
Securing the WebSocket session
Use wss:// so the WebSocket handshake and all subsequent frames are protected by TLS. Obtain a short-lived access token and send it in an authorization header during the handshake. Avoid credentials in the query string, where proxies and logs may retain them. The server must validate the token for every meaningful operation.
Treat inbound frames as untrusted input. Enforce message-size limits, parse them into typed models, reject unsuitable event types, and rate-limit reconnect attempts or commands. Refresh an expired token before retrying, and never let a client-side token decision authorise access to another user’s channel.
If a payload also carries language preferences, treat that field as display data rather than authentication; this background on locale identifier handling explains why locale values should not be used as session credentials. Keep personal information out of diagnostic logs and give users appropriate controls over data retained by the real-time service.
Testing and releasing the feature
Test against a small server that can delay frames, close connections, return malformed JSON, and replay missed events. Exercise reconnects on a physical Android phone as well as an emulator because emulators often use an uninterrupted virtual network. Combine server tests with instrumentation tests around the repository, state flow, and user interface.
For the Australian market, Google Play is the main Android storefront, with prices commonly displayed in AUD and Australian GST rules applied to eligible in-app purchases and subscriptions. Check Play Console’s country-specific pricing before release and use a staged rollout to limit disruption. Instrument handshake failures, reconnect counts, delivery lag, and events dropped after token expiry.
Keep production builds pointed at the production environment through build variants, never a developer machine. Connection health, accurate shared state, and graceful recovery should be measured as one feature. A staged release with live diagnostics provides evidence before wider availability.