Build a simple Android chat app with WebSockets
A chat screen feels instant because the server can push each new message to connected phones as it arrives. This Android tutorial builds that flow with Kotlin, OkHttp and a small Node.js relay, keeping the code understandable enough to adapt for a study group, support desk or local community project.
The app will use a persistent WebSocket for two-way communication, JSON for message data and a RecyclerView for the conversation. You will run the relay on your computer, test it from an emulator or handset, and then tighten the connection for deployment.
| Communication method | Connection model | Suitable use |
|---|---|---|
| Polling | Phone repeatedly requests updates | Small apps with infrequent messages |
| Server-sent events | Server pushes updates over one open connection | Notifications and live feeds |
| WebSockets | Persistent connection carries messages both ways | Interactive chat |
Plan the client and server
Start with three parts: the Android client, a WebSocket server and a shared message format. The server accepts connections and relays valid messages to every participant; it does not need a database for the first version.
Create an Android Studio project with an Activity, add the Internet permission and include OkHttp in the app-level Gradle file:
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.12.0")
}
Keep the socket logic in ChatSocket, the rows in MessageAdapter and screen actions in ChatActivity. Store the relay address in BuildConfig or a debug setting, because localhost on a phone refers to the phone rather than your development computer.
Define the message format
Use JSON instead of plain text so the client and server can distinguish a chat message from control events such as joining, leaving or typing. A minimal payload needs a type, display name, body and UTC timestamp:
{
"type": "message",
"user": "Mia",
"text": "G'day, is everyone connected?",
"sentAt": "2025-03-08T09:15:00Z"
}
The server should reject unknown types, missing text and oversized values before broadcasting anything. Escaping the text through JSON also prevents a message containing quotes or line breaks from breaking the protocol.
Store timestamps in UTC and format them using the device’s locale. This keeps offsets correct when daylight saving begins in Sydney or Melbourne, retains Adelaide’s half-hour time zone and leaves Perth on UTC+8. An Australian-friendly interface can use natural greetings such as “G’day” and “no worries” without hard-coding regional slang into the protocol.
Connect with OkHttp
OkHttp supports both ws:// and wss:// URLs through the same WebSocket API. The following client returns connection callbacks on Android’s main thread so the activity can update its views safely:
class ChatSocket(
private val onConnected: () -> Unit,
private val onText: (String) -> Unit,
private val onDisconnected: () -> Unit
) {
private val client = OkHttpClient.Builder().build()
private val main = Handler(Looper.getMainLooper())
private var socket: WebSocket? = null
fun connect(url: String) {
val request = Request.Builder().url(url).build()
socket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
main.post { onConnected() }
}
override fun onMessage(webSocket: WebSocket, text: String) {
main.post { onText(text) }
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(1000, reason)
main.post { onDisconnected() }
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
main.post { onDisconnected() }
}
})
}
fun send(text: String) = socket?.send(text) == true
fun close() = socket?.close(1000, "Leaving")
}
Create one instance in the activity and close it when the screen is destroyed. Show a connected or reconnecting label beside the message list, and let the activity decide when a retry should begin rather than creating repeated sockets inside the listener.
Create the chat interface
Use a vertical layout with a status label, a RecyclerView, a text field and a send button. Give the list enough weight to occupy the remaining space, constrain the input row to the bottom and set the list to stack from the newest item when the conversation starts.
An adapter can keep each item as a ChatMessage with the user, text and timestamp. Show the sender above each bubble, align your own messages to the right and include an accessible description such as “Message from Mia: G’day”. A ListAdapter with DiffUtil updates rows without resetting the whole conversation.
When the send button is pressed, remove blank text, build a JSONObject and pass it to ChatSocket.send(). Clear the field only after a successful send. Append the server’s echoed message to the adapter instead of adding it immediately on the phone; this avoids showing the same text twice while also confirming that other clients received it.
Run and test the exchange
Install Node.js, run npm init -y and add the ws package with npm install ws. This compact relay listens on port 8080, validates each payload and broadcasts it to every open client:
const { WebSocketServer, WebSocket } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", socket => {
socket.on("message", raw => {
let data;
try { data = JSON.parse(raw.toString()); } catch { return; }
if (data.type !== "message" || typeof data.text !== "string") return;
const message = {
type: "message",
user: String(data.user || "Guest").slice(0, 30),
text: data.text.slice(0, 500),
sentAt: new Date().toISOString()
};
const encoded = JSON.stringify(message);
for (const client of wss.clients) {
if (client.readyState === WebSocket.OPEN) client.send(encoded);
}
});
});
From the Android emulator, connect to ws://10.0.2.2:8080, which routes to the host computer. A physical handset needs your computer’s local network address, such as ws://192.168.1.42:8080, and both devices must share a network. Android blocks plain ws:// traffic by default on modern releases, so enable cleartext traffic only in the debug manifest for this local test; release builds should use wss://.
Launch two emulators, enter different names and send messages in both directions. Try “G’day from Melbourne” and “Arvo, it works from Perth”, then check that the status changes, timestamps use the correct local offset and the list scrolls to the newest bubble. This also confirms how Australian English and place-based sign-offs appear in a realistic conversation.
Harden the connection for real use
A development relay should not be exposed unchanged to the public internet. Validate message length and type on the server, apply per-connection rate limits, check the permitted origin during the handshake and place the service behind TLS with a valid certificate. The Android client should then use wss:// and reject plaintext URLs in release builds.
Networks change while people move, so treat a dropped socket as a normal event. Use bounded exponential backoff, refresh the connection when Android reports regained connectivity and avoid reconnecting forever after the user leaves the screen. These paths matter for commuters moving through Melbourne and Sydney as well as users on regional mobile broadband, where coverage may vary more sharply.
If the app later earns revenue, remember that the Google Play storefront in Australia prices eligible apps in Australian dollars, and digital features sold inside the app normally need Google Play Billing. Clear retention rules and suitable handling of personal information also fit Australian privacy expectations. A narrow protocol, observable connection state and carefully limited input give this small chat app a dependable base for Australian users from Brisbane to Perth.