Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
103: select Expression for Coroutines
A few years ago, I was working on a financial dashboard that needed to pull real-time exchange rates. We had two different API providers: one was incredibly fast but had a habit of timing out or returning 500s during peak volatility; the other was a rock-solid legacy system that was consistently slow. My teammate's initial approach was to call the fast one, wait for it to fail, and then call the slow one. The problem? When the fast API hung for 5 seconds before failing, the user just stared at a loading spinner, even though the slow API could have finished in 2 seconds. We needed a way to "race" the two calls and just take whoever crossed the finish line first.
This is exactly where the select expression comes in. In most of your coroutine work, you'll be using await() or receive(), which are blocking (in the suspending sense)—they stop execution until that one specific operation finishes. But select allows you to wait for multiple suspending operations simultaneously and execute a block of code for the first one that becomes available. It's essentially a type-safe way of saying, "I don't care who wins, just give me the first result that arrives."
Racing Deferred Results
When you're dealing with Deferred values (the results of async), you use the onAwait clause inside a select block. I like to think of this as a competition. Instead of waiting for deferredA and then deferredB, select monitors both. The moment one completes, its corresponding block runs, and the other operation is effectively ignored by the select block (though the coroutine is still running in the background unless you explicitly cancel it).
import kotlinx.coroutines.*
import kotlinx.coroutines.selects.*
suspend fun fetchFastApi() = async {
delay(1000) // Simulate network
"Fast API Result"
}
suspend fun fetchSlowApi() = async {
delay(2000) // Simulate network
"Slow API Result"
}
suspend fun raceApis() = coroutineScope {
val fast = fetchFastApi()
val slow = fetchSlowApi()
val winner = select<String> {
fast.onAwait { it }
slow.onAwait { it }
}
println("The winner is: $winner")
}
One detail you should notice: select is currently marked as @ExperimentalCoroutinesApi. Don't let that scare you off, but be aware that you'll likely need the annotation on your function. Honestly, it's been in this state for a while, and it's a staple for anyone building complex asynchronous pipelines.
Multiplexing with Channels
While racing Deferred values is common, select really shines when you're managing multiple Channels. Imagine you're building a chat application where you're listening to a socket for messages, but you also need to listen for a "cancel" signal from the UI thread. You can't call channel.receive() on the socket and then cancelChannel.receive() on the signal, because you'd be stuck waiting for a message before you'd ever check if the user clicked cancel.
By using onReceive (or onReceiveCatching for safer handling), you can merge these streams into a single loop. Here is how I usually structure that pattern:
val messages = Channel<String>()
val signals = Channel<Signal>()
while (isActive) {
val action = select<String> {
messages.onReceive { msg ->
"Processing message: $msg"
}
signals.onReceive { sig ->
"Handling signal: $sig"
}
}
println(action)
}
The beauty of this is that it keeps your logic sequential. You aren't juggling callbacks or managing complex state flags; you're simply reacting to whichever event happens first in time.
📋 Practical Task
Implement a Redundant Data Fetcher
You are building a system that must fetch a configuration file from two different mirrors. To ensure the lowest possible latency, you must launch requests to both mirrors simultaneously and return the result of whichever one responds first. If one fails, the select expression should still wait for the other (or you can handle the exception within the onAwait block).
Requirements:
- Create two functions,
fetchFromMirrorA()andfetchFromMirrorB(), both returning aDeferred<String>. - Simulate varying network speeds using
delay()with random values between 100ms and 1000ms. - Use a
selectexpression to capture the first successful response. - Print which mirror provided the result and the content of that result.
- Ensure the final code is wrapped in a
coroutineScopeto manage the lifecycle of the async tasks.
There are no comments for now.