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

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 datareceive()suspends if no data is availableChannels 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…



