-
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
180: Practice Exercise: Building a Coroutine-Based Download Manager
Imagine you're running a busy fast-food kitchen. If you had one cook who insisted on finishing a single burger—from grilling the patty to wrapping it in paper—before even looking at the next order, your customers would riot. Instead, the cook puts three patties on the grill, sets a timer, and while those are sizzling, they start prepping the buns or pouring drinks. They aren't doing ten things at the exact same microsecond, but they are managing multiple tasks in parallel, switching between them whenever one task is "waiting" (like waiting for the meat to cook).
This is exactly how we're going to approach our Download Manager. We aren't just launching one download and freezing the app; we're managing a "kitchen" of network requests.
Mapping the Kitchen to Kotlin
- The Orders: These are your individual download tasks. Each one is wrapped in a
Job. - The Grill Space: You can't download 1,000 files at once without the server blocking you or your RAM exploding. We'll use a
Semaphoreto limit how many coroutines can actually "work" at once. - The Timer: When a download is waiting for a packet of data from the web, the coroutine
suspends, freeing up the thread for other downloads. - Tossing the Order: If a user hits "Cancel," we don't just ignore the request; we call
job.cancel()to stop the data stream immediately.
Handling the Heavy Lifting with IO
Since we're dealing with network sockets and writing to disk, we cannot stay on the Main thread. I've seen too many developers accidentally freeze their UI because they forgot that InputStream.readBytes() is a blocking call. We'll wrap our actual download logic in withContext(Dispatchers.IO). This tells Kotlin, "Hey, move this specific piece of work to a thread pool optimized for waiting on hardware," keeping the UI buttery smooth.
Tracking Progress via Flows
A download manager that doesn't show a progress bar is just a black box that makes users nervous. To solve this, we aren't going to use old-school callbacks. Instead, we'll use a Flow. By emitting the percentage of bytes downloaded, the UI can simply collect those updates. It's a clean, reactive pipeline that handles the stream of data without leaking memory.
The Danger of Zombie Downloads
Here is a pro tip: always tie your downloads to a specific CoroutineScope. If the user navigates away from the download screen and you're using a global scope, those downloads keep running in the background, eating battery and data. By using a scope tied to the ViewModel or the Lifecycle, the moment the screen dies, the downloads are cancelled automatically. Don't be the dev who leaves "zombie" threads running in the background.
📋 Practical Task
Exercise: The Concurrent Asset Fetcher
Your task is to build a DownloadManager class that can handle multiple file URLs while restricting the number of simultaneous downloads to prevent network congestion.
Requirements:
- Create a class
DownloadManagerthat takes amaxConcurrentDownloadsinteger in its constructor. - Implement a function
downloadFile(url: String): Flow<DownloadStatus>. - Use a
Semaphoreto ensure that no more thanmaxConcurrentDownloadsare active at any given time. - Define a sealed class
DownloadStatuswith three states:Pending,Progress(percent: Int), andCompleted. - Simulate the download process using
delay()to represent network latency and a loop to emit progress percentages (e.g., 0%, 25%, 50%, 75%, 100%). - Ensure that if the
Jobassociated with the flow collection is cancelled, the "download" stops immediately.
Starter Code Snippet:
sealed class DownloadStatus {
object Pending : DownloadStatus()
data class Progress(val percent: Int) : DownloadStatus()
object Completed : DownloadStatus()
}
class DownloadManager(private val maxConcurrentDownloads: Int) {
private val semaphore = Semaphore(maxConcurrentDownloads)
fun downloadFile(url: String): Flow<DownloadStatus> = flow {
// Your implementation here
}
}There are no comments for now.