Skip to Content
Course content

72: Practice Exercise: Building a Coroutine-Based Data Fetcher

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

Imagine you're organizing a dinner party. You need a cake from the bakery, flowers from the florist, and wine from the cellar. If you did this sequentially, you'd drive to the bakery, wait in line, drive home, then drive to the florist, wait in line, drive home, and finally go to the cellar. It's a waste of time. You're the bottleneck.

Instead, you'd probably call a couple of friends. You tell Friend A to get the cake, Friend B to get the flowers, and you handle the wine yourself. You're all working in parallel. The party doesn't start until everyone is back, but the total time spent waiting is only as long as the slowest person's trip, not the sum of all three. This is exactly how a coroutine-based data fetcher operates.

Mapping the Party to the Code

In Kotlin, we can map this logic directly to our concurrency primitives:

  • The Friends: These are your async blocks. They start a task in the background and return a Deferred value (a promise that the "cake" will eventually arrive).
  • The Phone Calls: This is the coroutineScope. It ensures that if one friend has a total meltdown (an exception), you can handle it or cancel the other requests so you aren't waiting for flowers when the cake is already ruined.
  • Waiting at the Door: This is the await() call. You can't start the party until you actually have the items in hand.
  • The Car/Road: This is Dispatchers.IO. We don't want to do network calls on the Main thread (the "living room" of your app) because it would freeze the UI.

Firing off Requests in Parallel

I've seen too many developers use launch when they actually need async. Remember: use launch for "fire and forget" tasks. Use async when you actually need a result back. When building a data fetcher, you almost always want async.

suspend fun fetchUserDashboard(userId: String): DashboardData = coroutineScope {
    // We start these immediately. They run concurrently.
    val profileDeferred = async(Dispatchers.IO) { api.getProfile(userId) }
    val ordersDeferred = async(Dispatchers.IO) { api.getRecentOrders(userId) }
    val settingsDeferred = async(Dispatchers.IO) { api.getSettings(userId) }

    // Now we wait for all of them to finish. 
    // The total time is the time of the slowest request.
    DashboardData(
        profile = profileDeferred.await(),
        orders = ordersDeferred.await(),
        settings = settingsDeferred.await()
    )
}

Dealing with the "Burnt Cake" Scenario

In the real world, APIs fail. If the getRecentOrders call throws an exception, the default behavior of coroutineScope is to cancel all other children and throw the exception up the chain. Sometimes that's what you want, but for a dashboard, you might still want to show the user's profile even if their order history is temporarily unavailable.

To handle this, I recommend wrapping your await() calls in a try-catch or using a runCatching block. However, be careful: if you wrap the async block itself in a try-catch, the exception is caught inside the coroutine, and await() will return the result of that catch block. This is usually the cleaner way to provide "fallback" data.

val ordersDeferred = async(Dispatchers.IO) {
    try {
        api.getRecentOrders(userId)
    } catch (e: Exception) {
        emptyList() // Fallback to an empty list if the API is down
    }
}

By structuring your fetcher this way, you're maximizing the efficiency of the device's radio and CPU, providing a snappier experience for the user. You aren't blocking threads; you're just suspending execution until the data is ready.




📋 Practical Task

Exercise: Building a Concurrent User Dashboard Aggregator

Your task is to implement a DashboardService that fetches data from three different simulated sources concurrently. You must ensure that the total execution time is roughly equal to the slowest request, not the sum of all requests.

Requirements:

  • Create a data class UserDashboard(val name: String, val balance: Double, val notifications: Int).
  • Implement a function fetchDashboardData(userId: String): UserDashboard using coroutineScope and async.
  • Simulate three separate API calls using delay()`:
    • fetchName(id): takes 300ms.
    • fetchBalance(id): takes 800ms.
    • fetchNotifications(id): takes 500ms.
  • Error Handling: If fetchNotifications fails (simulate this by randomly throwing an IOException), the dashboard should still be returned, but with the notifications count set to 0.
  • The final execution time for a successful fetch should be approximately 800ms.

Starter Code:


class DashboardService {
    suspend fun fetchName(id: String): String {
        delay(300)
        return "Jane Doe"
    }

    suspend fun fetchBalance(id: String): Double {
        delay(800)
        return 1250.50
    }

    suspend fun fetchNotifications(id: String): Int {
        delay(500)
        if (Math.random() > 0.7) throw IOException("Network Error")
        return 5
    }

    suspend fun fetchDashboardData(userId: String): UserDashboard {
        // TODO: Implement using async and await
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.