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

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Search for a command to run...

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
No comments yet. Be the first to comment.
A practical guide to choosing the right coroutine primitive for your Android/Kotlin projects

Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

HandlerInterceptor vs Filter in Spring Boot: A Practical Guide with JNDI Injection Prevention

Simple In-Memory Caching: A Tiny Trick with Massive Impact

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.
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.
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
No buffer (capacity = 0)
Sender and receiver must meet
Guarantees backpressure
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"
}
Strict one-to-one communication
You want producer to slow down if consumer is not ready
Event-style handoff

Holds multiple values
Producer can run ahead (up to capacity)
Reduces suspension overhead
val channel = Channel<Int>(capacity = 10)
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")
}
Logging
Analytics
Background batching
Task queues
One producer
Many consumers
Each item processed once
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")
}
Image processing
Parallel API handling
Background job systems

Stores only the most recent value
Older values are dropped
Great for state updates
val channel = Channel<Int>(Channel.CONFLATED)
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%")
}
}
Progress bars
Location updates
Live status indicators
select)You want to react to whichever event happens first.
Use select {}.
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)
}
Competing API responses
Cancellation signals
Priority-based event handling
channel.close()
for (x in channel) to consume safelyfor (item in channel) {
process(item)
}
try {
for (item in channel) {
process(item)
}
} finally {
cleanup()
}

Use StateFlow, not Channel.
Bad:
Channel<UserState>
Good:
StateFlow<UserState>
Channels deliver each value to one receiver only.
If everyone must see everything → use Flow.
Channels do NOT replay values.
If new subscribers need old data → use Flow / SharedFlow.
This is wrong:
val channel = Channel<Int>()
This is better:
suspend fun load(): Int
Channel =
- point-to-point communication
- one value goes to one consumer
- designed for coordination and work sharing
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…