-
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
234: Building a Simple Job Scheduler with Coroutines
I've always found that the best way to understand Coroutine orchestration is to try and build something that feels like it should be easy, only to realize that "easy" usually means "I forgot how to stop it." Today, let's build a simple Job Scheduler. Imagine we're building a telemetry system for a drone; we need to send battery levels every 2 seconds and GPS coordinates every 5 seconds.
The "Just Loop It" Approach
My first instinct is usually to just throw a while(true) loop inside a coroutine. It's the path of least resistance. Let's see what happens when I try to launch a couple of these sync tasks.
val scope = CoroutineScope(Dispatchers.Default)
fun startSync(taskName: String, intervalMs: Long) {
scope.launch {
while (true) {
println("[$taskName] Syncing data at ${System.currentTimeMillis()}")
delay(intervalMs)
}
}
}
// Triggering them
startSync("Battery", 2000)
startSync("GPS", 5000)
At first glance, this is great. It's non-blocking, it's lightweight, and the logs are filling up exactly as expected. But here's the problem: I have no way to stop the Battery sync without killing the entire scope. In a real app, if the user toggles "Battery Monitoring" off, this loop just keeps chewing through CPU and battery in the background. I've essentially created a leak.
Getting a Grip on the Job
To fix this, I need a handle. In Kotlin, launch returns a Job. If I keep a reference to that Job, I can call cancel() on it. I'll try modifying the function to return that Job so the caller can manage it.
fun startSync(taskName: String, intervalMs: Long): Job {
return scope.launch {
while (isActive) { // Using isActive instead of true is safer for cancellation
println("[$taskName] Syncing data...")
delay(intervalMs)
}
}
}
val batteryJob = startSync("Battery", 2000)
// Later...
batteryJob.cancel()
That's better. I used isActive in the loop—which is a property of the CoroutineScope—to ensure the loop terminates immediately when the job is cancelled. But wait, if I'm building a scheduler, I don't want the UI layer or the calling service to have to manually track five different Job variables. That's a recipe for a mess.
Centralizing the Schedule
I'm thinking we should wrap this in a class that manages a map of jobs. This way, I can say "Start the GPS task," and if the GPS task is already running, I can either ignore the request or restart it. Let's go with the "replace" logic—if you reschedule a task, the old one should die first.
class JobScheduler(private val scope: CoroutineScope) {
private val activeJobs = mutableMapOf<String, Job>()
fun schedule(id: String, intervalMs: Long, action: suspend () -> Unit) {
// If there's already a job with this ID, kill it first
activeJobs[id]?.cancel()
activeJobs[id] = scope.launch {
while (isActive) {
action()
delay(intervalMs)
}
}
}
fun cancelJob(id: String) {
activeJobs[id]?.cancel()
activeJobs.remove(id)
}
}
Now this feels like a professional tool. I can pass in any suspend function as the action. I'm not tied to println anymore; I could be hitting a Retrofit API or writing to a Room database.
Dealing with the "Overlapping" Problem
There's one flaw here. What happens if the action() takes longer than the intervalMs? If the GPS sync takes 6 seconds but the interval is 5 seconds, the delay(intervalMs) only happens after the action finishes. This means my tasks won't "drift," but they also won't be strictly periodic.
If I wanted them to be strictly periodic regardless of how long the work takes, I'd have to launch the work in a separate child coroutine inside the loop. But honestly? In 90% of software engineering cases, the "sequential" approach I wrote above is actually what you want. It prevents the system from piling up a backlog of unfinished sync tasks if the network gets slow. I'll stick with the sequential version for now to keep the system stable.
📋 Practical Task
Build a Telemetry Heartbeat Manager
Create a class called HeartbeatManager that uses a CoroutineScope to manage multiple background heartbeats. Your manager must implement the following requirements:
- A function
startHeartbeat(deviceId: String, interval: Long)that starts a loop printing "Heartbeat from [deviceId]" to the console. - The manager must ensure that if
startHeartbeatis called again with the samedeviceId, the previous heartbeat for that device is cancelled before the new one starts. - A function
stopHeartbeat(deviceId: String)that halts the loop for a specific device. - A function
stopAll()that cancels every active heartbeat currently being managed.
Test your implementation by starting three different device heartbeats at different intervals, stopping one specifically, and then calling stopAll() to ensure no coroutines are left leaking.
There are no comments for now.