-
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
237: Building a Podcast Downloader CLI with Coroutines
I've been wanting to build a quick CLI tool to archive some of my favorite podcast episodes before the hosts take them offline. Since we're dealing with network I/O—which is essentially just waiting around for a server to send us bytes—this is the perfect scenario to put our coroutine knowledge to work. I don't want to wait for episode one to finish before starting episode two.
The slow way: sequential downloads
I'll start with the most obvious approach. I have a list of URLs, and I'll just loop through them. I've written a helper function downloadFile that simulates a network request with a delay.
suspend fun downloadFile(url: String): String {
println("Starting download: $url")
delay(1000) // Simulating network lag
return "Content from $url"
}
fun main() = runBlocking {
val urls = listOf("ep1.mp3", "ep2.mp3", "ep3.mp3")
val results = mutableListOf<String>()
for (url in urls) {
results.add(downloadFile(url))
}
println("Downloaded ${results.size} episodes.")
}
When I run this, it works, but it's painfully slow. It takes exactly three seconds because it's downloading them one by one. It's basically synchronous code wearing a suspend hat. I'm not actually utilizing the power of coroutines here; I'm just pausing the main thread repeatedly.
Where did the files go?
Okay, my first instinct is to just wrap the call in launch. I want these to fire off immediately without waiting for the previous one to finish. Let's try this:
fun main() = runBlocking {
val urls = listOf("ep1.mp3", "ep2.mp3", "ep3.mp3")
for (url in urls) {
launch {
downloadFile(url)
}
}
println("Downloads started!")
}
Now it's fast. The "Downloads started!" message prints almost instantly. But there's a problem: if this were a real CLI tool and I wasn't using runBlocking at the top level, or if I used GlobalScope, the program would likely terminate before the downloads even finished. Even here, I have no way to know when they're actually done or if they succeeded. I've traded control for speed, and in a production tool, that's a recipe for corrupted files.
Getting them to run in parallel
I need a way to kick off all these downloads and then wait for all of them to return a result. This is where async comes in. Unlike launch, which is "fire and forget," async returns a Deferred—basically a promise that a value will eventually exist.
Let's adjust the logic. I'll map the URLs to a list of Deferred objects, and then await them all at once.
fun main() = runBlocking {
val urls = listOf("ep1.mp3", "ep2.mp3", "ep3.mp3")
val deferredDownloads = urls.map { url ->
async {
downloadFile(url)
}
}
// awaitAll() is a handy extension function for collections of Deferred
val results = deferredDownloads.awaitAll()
println("Successfully downloaded ${results.size} episodes.")
}
This is much better. The downloads happen concurrently, but the awaitAll() call ensures the program doesn't move past that point until every single episode is safely on disk. I'm seeing the "Starting download" logs appear almost simultaneously now.
Dealing with the occasional 404
In the real world, a podcast feed might have a dead link. Right now, if one async block throws an exception, it will propagate up and cancel all the other sibling coroutines in the scope. That's standard structured concurrency, but it's annoying here—I don't want one bad URL to stop me from getting the other ten episodes.
To fix this, I'll wrap the internal logic of the async block in a try-catch. I'll return null for failed downloads so I can filter them out later.
fun main() = runBlocking {
val urls = listOf("ep1.mp3", "broken_link.mp3", "ep3.mp3")
val deferredDownloads = urls.map { url ->
async {
try {
downloadFile(url)
} catch (e: Exception) {
println("Failed to download $url: ${e.message}")
null
}
}
}
val results = deferredDownloads.awaitAll().filterNotNull()
println("Downloaded ${results.size} out of ${urls.size} episodes.")
}
Now the tool is resilient. It attempts everything, reports the failures, and still gives me the successful downloads. This pattern—map to async, followed by awaitAll and a filterNotNull—is my go-to for batch processing network requests in Kotlin.
📋 Practical Task
Build a Resilient Podcast Batch Downloader
Create a Kotlin program that simulates downloading a list of podcast episodes. Your program must meet the following requirements:
- Define a
suspendfunctionfetchEpisode(url: String)that simulates a network delay usingdelay(). - Randomly simulate a failure in
fetchEpisode(e.g., usingRandom.nextBoolean()or checking if the URL contains the word "error") by throwing anIOException. - Use
asyncandawaitAll()to download a list of at least 5 URLs concurrently. - Ensure that a failure in one download does not stop others from completing.
- Print the final count of successfully downloaded episodes versus the total number attempted.
There are no comments for now.