Kotlin
Completed
-
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
100: CoroutineContext and Job Hierarchy
You've probably already used launch and async to get things running in the background, but there is a subtle architectural shift that happens when you move from "making it work" to "making it production-ready." That shift is understanding the CoroutineContext and, more importantly, the Job hierarchy. If you treat coroutines as isolated threads, you're going to run into "ghost" processes—tasks that keep churning away in the background long after the user has closed the screen or the network request has timed out.
The "Fire and Forget" Fallacy
Early on, I see a lot of developers reach for GlobalScope.launch. It feels intuitive. You have a task—say, uploading a set of logs to a server—and you just want it to happen without blocking the UI. You think, "I'll just throw this into the GlobalScope and let it run."
// The naive way: No one is watching these jobs
fun uploadLogs(logs: List<Log>) {
logs.forEach { log ->
GlobalScope.launch(Dispatchers.IO) {
api.send(log)
}
}
}
Here is the problem: GlobalScope creates top-level coroutines. They have no parent. Because they have no parent, they aren't bound to any lifecycle. If the user logs out or the service is destroyed, these uploads keep running. Worse, if one of those uploads crashes with an exception, you have no structured way to catch it or cancel the remaining siblings. You've essentially created a bunch of orphans that you can't track, manage, or kill. It's a memory leak waiting to happen.
Connecting the Dots with Job Hierarchy
In Kotlin, the CoroutineContext is more than just a way to set the Dispatcher. It's a map that contains a Job. When you launch a coroutine inside another coroutine's scope, the new coroutine inherits the context of its parent, but it creates its own Job that becomes a child of the parent's Job. This is the "Hierarchy" part of the equation.
If we refactor that log uploader to use a proper scope—or better yet, a coroutineScope builder—the behavior changes entirely. I prefer using coroutineScope when I need to group multiple concurrent operations that must all complete before the function returns.
// The professional way: Structured Concurrency
suspend fun uploadLogs(logs: List<Log>) = coroutineScope {
logs.forEach { log ->
launch(Dispatchers.IO) {
api.send(log)
}
}
// The function won't return until all launched children are done
}
By using coroutineScope, you've created a boundary. If the parent scope that called uploadLogs is cancelled, all the individual log uploads are cancelled instantly. There's no guesswork. The Job hierarchy ensures that cancellation propagates downwards. If the parent dies, the children die. It's a clean, predictable tree.
When One Failure Shouldn't Kill Everything
Now, there's a catch. By default, if one child in this hierarchy fails with an exception, it cancels its parent, which in turn cancels all other children. In a log uploader, if log #2 fails because of a malformed string, you probably don't want logs #3 through #100 to be aborted. This is where the standard Job hierarchy can feel too aggressive.
To fix this, we use a SupervisorJob. A supervisor changes the rule: failure of a child doesn't propagate upwards to the parent. I usually implement this by using supervisorScope. It gives you the same structured concurrency (waiting for children to finish) but prevents a single failing task from nuking the entire operation.
The trade-off is simple: use coroutineScope when the tasks are interdependent (if one fails, the whole operation is useless); use supervisorScope when the tasks are independent (if one fails, the others should keep going). Understanding this distinction is what separates a junior's "it works on my machine" code from a senior's resilient system.
📋 Practical Task
Refactoring the Leaky Image Processing Pipeline
You are reviewing a teammate's code for an image editor. They've implemented a processImage function that applies three different filters (Blur, Contrast, and Saturation) in parallel. However, they used GlobalScope, meaning if the user hits "Cancel" or leaves the screen, the CPU-intensive filtering keeps running in the background.
Your Task: Refactor the following code to use structured concurrency. Replace GlobalScope with a coroutineScope or supervisorScope. Ensure that if the main processImage call is cancelled, all three filter jobs are also cancelled immediately.
suspend fun processImage(image: Bitmap) {
// This is the problematic implementation
GlobalScope.launch(Dispatchers.Default) {
applyBlur(image)
}
GlobalScope.launch(Dispatchers.Default) {
applyContrast(image)
}
GlobalScope.launch(Dispatchers.Default) {
applySaturation(image)
}
println("Filters started...")
// Problem: This prints immediately, and the function returns
// while the filters are still running in the background!
}
Requirements:
- The function must not return until all three filters have completed.
- The filters must run concurrently (not sequentially).
- The implementation must be cancellable from the caller's side.
There are no comments for now.