Updated 13 days ago | GitHub

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(), or OutputStream.write(...) on the main thread raises NetworkOnMainThreadException; 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 with javax.net.ssl.SSLSocketFactory so 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 Callback posted with Handler.post(...) rather than onPostExecute.
  • Try-with-resources closes the Socket, PrintWriter, and BufferedReader on 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