Skip to main content

Command Palette

Search for a command to run...

Kotlin Channels: A Simple, Practical Guide (Beginner → Advanced)

Updated
4 min read
Kotlin Channels: A Simple, Practical Guide (Beginner → Advanced)
R

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.

This guide explains what Channels are, when to use them, how to use them correctly, and when NOT to use them — in simple, professional language.

1. What problem do Channels solve?

In Kotlin coroutines, you often have multiple coroutines running at the same time.

Sometimes one coroutine:

  • produces data (events, tasks, values)

  • another coroutine consumes that data

You need a safe, suspendable way to pass data between them.

👉 Channel is Kotlin’s solution for this.

2. What is a Channel (simple definition)

A Channel is a thread-safe communication primitive used to:

  • send values from one coroutine

  • receive those values in another coroutine

Key properties:

  • send() suspends if the channel cannot accept data

  • receive() suspends if no data is available

  • Channels respect coroutine cancellation

3. Basic Channel (Rendezvous)

Characteristics

  • No buffer (capacity = 0)

  • Sender and receiver must meet

  • Guarantees backpressure

Example: Background task → UI layer

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

fun main() = runBlocking {
    val resultChannel = Channel<String>()

    // Background work
    launch(Dispatchers.Default) {
        val result = heavyComputation()
        resultChannel.send(result) // suspends until received
    }

    // UI or caller
    launch {
        val value = resultChannel.receive()
        println("Result received: $value")
    }
}

fun heavyComputation(): String {
    Thread.sleep(500)
    return "Success"
}

When this is good

  • Strict one-to-one communication

  • You want producer to slow down if consumer is not ready

  • Event-style handoff

4. Buffered Channel (Queue behavior)

Characteristics

  • Holds multiple values

  • Producer can run ahead (up to capacity)

  • Reduces suspension overhead

val channel = Channel<Int>(capacity = 10)

Example: Logging system

fun main() = runBlocking {
    val logChannel = Channel<String>(capacity = 50)

    // Log producer (fast)
    launch {
        repeat(100) {
            logChannel.send("Log message #$it")
        }
        logChannel.close()
    }

    // Log consumer (slow IO)
    launch(Dispatchers.IO) {
        for (log in logChannel) {
            writeLogToDisk(log)
        }
    }
}

fun writeLogToDisk(log: String) {
    Thread.sleep(50)
    println("Written: $log")
}

When to use

  • Logging

  • Analytics

  • Background batching

  • Task queues

5. Channel as a Work Queue (Multiple Consumers)

Pattern

  • One producer

  • Many consumers

  • Each item processed once

Example: Processing network requests

fun main() = runBlocking {
    val requestChannel = Channel<Int>(capacity = 20)

    // Producer
    launch {
        repeat(10) {
            requestChannel.send(it)
        }
        requestChannel.close()
    }

    // Workers
    repeat(3) { workerId ->
        launch {
            for (request in requestChannel) {
                handleRequest(workerId, request)
            }
        }
    }
}

fun handleRequest(workerId: Int, request: Int) {
    Thread.sleep(200)
    println("Worker $workerId handled request $request")
}

Use cases

  • Image processing

  • Parallel API handling

  • Background job systems

6. Conflated Channel (Only latest value matters)

Characteristics

  • Stores only the most recent value

  • Older values are dropped

  • Great for state updates

val channel = Channel<Int>(Channel.CONFLATED)

Example: Progress updates

fun main() = runBlocking {
    val progressChannel = Channel<Int>(Channel.CONFLATED)

    // Producer
    launch {
        for (i in 0..100 step 5) {
            progressChannel.send(i)
        }
        progressChannel.close()
    }

    // Consumer
    for (progress in progressChannel) {
        println("UI progress updated: $progress%")
    }
}

Use cases

  • Progress bars

  • Location updates

  • Live status indicators

7. Listening to multiple Channels (select)

Problem

You want to react to whichever event happens first.

Solution

Use select {}.

Example: Data or shutdown signal

import kotlinx.coroutines.selects.select

fun main() = runBlocking {
    val dataChannel = Channel<String>()
    val shutdownChannel = Channel<Unit>()

    launch {
        dataChannel.send("New data")
    }

    launch {
        delay(300)
        shutdownChannel.send(Unit)
    }

    val result = select<String> {
        dataChannel.onReceive {
            "Data received: $it"
        }
        shutdownChannel.onReceive {
            "Shutdown requested"
        }
    }

    println(result)
}

Use cases

  • Competing API responses

  • Cancellation signals

  • Priority-based event handling

8. Closing, Cancellation, and Safety (VERY IMPORTANT)

Rule 1: Always close channels you own

channel.close()

Rule 2: Use for (x in channel) to consume safely

for (item in channel) {
    process(item)
}

Rule 3: Respect cancellation

try {
    for (item in channel) {
        process(item)
    }
} finally {
    cleanup()
}

9. When NOT to use Channels ❌ (Very important)

❌ Do NOT use Channels when:

1. You need state, not events

Use StateFlow, not Channel.

Bad:

Channel<UserState>

Good:

StateFlow<UserState>

2. You need multiple collectors to receive all values

Channels deliver each value to one receiver only.

If everyone must see everything → use Flow.

3. You need replay or caching

Channels do NOT replay values.

If new subscribers need old data → use Flow / SharedFlow.

4. Simple suspend → return is enough

This is wrong:

val channel = Channel<Int>()

This is better:

suspend fun load(): Int

10. Mental Model (simple and correct)

Channel =
- point-to-point communication
- one value goes to one consumer
- designed for coordination and work sharing

11. Final Summary

Use Channels when you need:

  • Coroutine-to-coroutine communication

  • Work queues

  • Event pipelines

  • Backpressure

Do NOT use Channels when you need:

  • Shared state

  • Replay

  • Multiple observers

  • UI state management


Cheat Sheet:


That’s it for today. Happy coding…