Sending and Receiving Data with Sockets
Overview
Network sockets are endpoints of TCP/UDP connections between devices. An Android app typically acts as the client, opening a java.net.Socket to a server and exchanging bytes or text over its input/output streams. This page shows two modern ways to drive that socket off the UI thread: Kotlin coroutines with Dispatchers.IO (the recommended path), and an ExecutorService plus Handler for Java callers.
AsyncTask was the historical answer for pushing socket work onto a background thread on Android. It was deprecated in API 30 (Android 11) with the official guidance to “use the standard java.util.concurrent or Kotlin concurrency utilities instead.” If you are maintaining a codebase that still uses it, the patterns below are the canonical replacements.
Manifest and runtime constraints
<uses-permission android:name="android.permission.INTERNET" />
Two runtime constraints apply on every supported Android version:
- All blocking socket I/O must run off the UI thread. Calling
Socket(...),readLine(), orOutputStream.write(...)on the main thread raisesNetworkOnMainThreadException; the StrictMode network policy enforces this on every release since API 11. - Cleartext (non-TLS) TCP is blocked by default on API 28+. For a plain-TCP server on a development LAN, either set
android:usesCleartextTraffic="true"on<application>(development only) or whitelist the host through a Network Security Configuration. For production, wrap the socket withjavax.net.ssl.SSLSocketFactoryso the link uses TLS.
TCP client with Kotlin coroutines (recommended)
Coroutines are the officially recommended Android concurrency primitive — see the Kotlin coroutines on Android guide. Use Dispatchers.IO for the blocking socket call, expose the operation as a suspend function, and launch it from a lifecycle-aware scope such as viewModelScope.
Add the dependencies:
// app/build.gradle.kts
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
}
The socket call
TcpClient opens a connection, writes a single command, and returns the first line of the response. Socket.use { ... } closes the socket on every exit path, including cancellation.
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.Socket
class TcpClient(private val host: String, private val port: Int) {
/**
* Sends [command] to the server and returns the first line of the response.
* Main-safe: suspends on Dispatchers.IO and never blocks the caller's thread.
*/
suspend fun send(command: String): String = withContext(Dispatchers.IO) {
Socket(host, port).use { socket ->
val writer = PrintWriter(socket.getOutputStream(), /* autoFlush = */ true)
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
writer.println(command)
reader.readLine().orEmpty()
}
}
}
withContext(Dispatchers.IO) moves the blocking work to the IO dispatcher’s thread pool, which is sized for blocking I/O (64 threads or core count, whichever is larger). The function is main-safe: a caller on the main dispatcher can await it without freezing the UI.
Driving it from a ViewModel
viewModelScope cancels the coroutine — and unwinds the use { ... } block, which closes the socket — when the ViewModel is cleared.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ShutdownViewModel : ViewModel() {
private val client = TcpClient(host = "192.168.1.1", port = 4444)
private val _state = MutableStateFlow<UiState>(UiState.Idle)
val state: StateFlow<UiState> = _state.asStateFlow()
fun sendShutdown() {
viewModelScope.launch {
_state.value = UiState.Connecting
_state.value = try {
val response = client.send("shutdown")
if (response == "ok") UiState.Sent else UiState.Error(response)
} catch (t: Throwable) {
UiState.Error(t.message ?: "unknown error")
}
}
}
}
sealed interface UiState {
data object Idle : UiState
data object Connecting : UiState
data object Sent : UiState
data class Error(val reason: String) : UiState
}
Collect the state flow from your Activity/Fragment (with collectAsStateWithLifecycle() in Compose, or repeatOnLifecycle(STARTED) in a View-based UI) to drive the UI. Configuration changes do not interrupt the in-flight request because the ViewModel outlives them.
Long-lived bidirectional connections
For a connection that stays open and pushes messages from the server, expose the input stream as a Flow and let the collector’s lifecycle drive cancellation:
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.isActive
fun TcpClient.receive(): Flow<String> = flow {
Socket(host, port).use { socket ->
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
while (currentCoroutineContext().isActive) {
val line = reader.readLine() ?: break
emit(line)
}
}
}.flowOn(Dispatchers.IO)
Cancelling the collector cancels the coroutine, which unwinds Socket.use and closes the connection.
TCP client with ExecutorService (Java)
For Java-only modules the official Android threading guide recommends a shared ExecutorService plus a Handler bound to Looper.getMainLooper() for delivering results back to the UI thread.
import android.os.Handler;
import android.os.Looper;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TcpClient {
public interface Callback {
void onResult(String response);
void onError(Throwable error);
}
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final Handler MAIN = new Handler(Looper.getMainLooper());
private final String host;
private final int port;
public TcpClient(String host, int port) {
this.host = host;
this.port = port;
}
public void send(final String command, final Callback callback) {
EXECUTOR.execute(() -> {
try (Socket socket = new Socket(host, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
out.println(command);
final String response = in.readLine();
MAIN.post(() -> callback.onResult(response == null ? "" : response));
} catch (Exception e) {
MAIN.post(() -> callback.onError(e));
}
});
}
}
Key differences from the deprecated AsyncTask form:
- One shared executor handles every request instead of a fresh subclass per call.
- Result delivery is a
Callbackposted withHandler.post(...)rather thanonPostExecute. - Try-with-resources closes the
Socket,PrintWriter, andBufferedReaderon every exit path.
If the Callback is held by an Activity or Fragment, invalidate it in onDestroy so a slow request cannot deliver into a torn-down view. A ViewModel caller does not have this problem because it survives configuration changes.
A note on Android servers
ServerSocket works on Android, but a server in an app process is rarely a good fit: the OS can kill the process whenever the app is backgrounded, the device’s IP changes as it roams Wi-Fi networks, and a long-lived listener is hostile to battery. If you need a listener for testing, run the ServerSocket from inside a foreground service so the OS will not reclaim it. For production, terminate the connection on a server you control and have the device act as the client.
References
- Threading in Android — official background-work guide.
- Kotlin coroutines on Android — recommended replacement for
AsyncTask. AsyncTask— deprecation notice and recommended alternatives.Dispatchers.IO— coroutine dispatcher for blocking I/O.java.net.Socket— TCP socket reference.NetworkOnMainThreadException— runtime guard against network calls on the UI thread.- Network security configuration — controls cleartext traffic on API 28+.
- Oracle: Custom Networking — generic Java socket tutorial.