Skip to content

Sending and Receiving Data with Sockets

codepath-wiki-review[bot] edited this page Jul 9, 2026 · 13 revisions

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.11.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel:2.11.0")
}

The lifecycle-viewmodel-ktx artifact has been empty since Lifecycle 2.8.0, with viewModelScope and other Kotlin extensions folded into the base lifecycle-viewmodel artifact — depend on the base artifact directly for new projects.

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

Finding these guides helpful?

We need help from the broader community to improve these guides, add new topics and keep the topics up-to-date. See our contribution guidelines here and our topic issues list for great ways to help out.

Check these same guides through our standalone viewer for a better browsing experience and an improved search. Follow us on twitter @codepath for access to more useful Android development resources.

Clone this wiki locally