-
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
72: Practice Exercise: Building a Coroutine-Based Data Fetcher
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
asyncblocks. They start a task in the background and return aDeferredvalue (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): UserDashboardusingcoroutineScopeandasync. - Simulate three separate API calls using
delay()`:fetchName(id): takes 300ms.fetchBalance(id): takes 800ms.fetchNotifications(id): takes 500ms.
- Error Handling: If
fetchNotificationsfails (simulate this by randomly throwing anIOException), the dashboard should still be returned, but with the notifications count set to0. - 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
}
}
There are no comments for now.