Skip to Content
Course content

180: Practice Exercise: Building a Coroutine-Based Download Manager

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Semaphore to 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 DownloadManager that takes a maxConcurrentDownloads integer in its constructor.
  • Implement a function downloadFile(url: String): Flow<DownloadStatus>.
  • Use a Semaphore to ensure that no more than maxConcurrentDownloads are active at any given time.
  • Define a sealed class DownloadStatus with three states: Pending, Progress(percent: Int), and Completed.
  • 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 Job associated 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
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.