Yes, we're now running our Black Friday Sale. All Access and Pro are 33% off until 2nd December, 2025:
Implement Scheduler/Timer with Kotlin Coroutine
Last updated: September 16, 2024
1. Introduction
In Kotlin, coroutines offer a powerful and efficient way to handle asynchronous programming. These include scheduling repetitive tasks or setting up timers, and Kotlin’s coroutines provide several ways to implement these.
In this tutorial, we’ll explore creating schedulers and timers using Kotlin coroutines.
2. Basics of Kotlin Coroutines
Before diving into scheduling, it’s essential to have a basic understanding of Kotlin coroutines. Kotlin coroutines provide a way to write asynchronous code sequentially, making it easier to handle tasks such as background work, networking, or timers. Some key coroutine functions include:
- launch(): starts a new coroutine that doesn’t return a result
- async(): starts a new coroutine that returns a Deferred result
- delay(): pauses the coroutine for a specified duration without blocking the thread
- isActive: a property that returns true if the coroutine is still running; developers commonly use it in loops to determine whether a coroutine should continue executing
We’ll leverage these basic coroutine functions to implement our scheduler.
3. Implementing a Simple Timer with delay()
A timer is a simple tool that waits for a set amount of time before executing a task once. Unlike schedulers, which repeat tasks at defined intervals, a timer only triggers the task once after the specified delay.
Kotlin’s delay() function allows a coroutine to suspend for a specific amount of time, making it ideal for this kind of task.
Let’s create a basic timer that waits for the specified delay before executing the provided task:
fun CoroutineScope.startTimer(delay: Long = 1000L, task: suspend () -> Unit): Job {
return launch {
delay(delay)
task()
}
}
This function runs the provided task after the specified delay, executing the task only once. It returns the coroutine’s Job, which can be canceled if necessary before the task runs.
Now, let’s test our timer:
@Test
fun `timer executes task after delay`() = runBlocking {
var taskExecuted = false
val task: suspend () -> Unit = {
taskExecuted = true
}
startTimer(delay = 1000L, task = task)
delay(1500L)
assertTrue(taskExecuted)
}
In this test, the timer briefly waits before executing the task. Because the timer is asynchronous, we also have to wait before we can make any assertions so that we know our timer has run.
This timer implementation is useful for cases where we need a single deferred action, such as setting timeouts or handling one-time events.
4. Scheduling Repetitive Tasks with Coroutines
While a timer executes a task once after a set delay, there are many situations where tasks must run continuously at regular intervals for as long as the coroutine remains active. This is common in scenarios like background updates or polling services.
In Kotlin, this can be achieved using a loop that checks the isActive property from the CoroutineScope and ensures that the task runs while the coroutine is alive, exiting gracefully when the coroutine is canceled.
Let’s create a scheduler that runs a task repeatedly with a specified interval, continuing until the coroutine is canceled:
fun CoroutineScope.startInfiniteScheduler(interval: Long = 1000L, task: suspend () -> Unit): Job {
return launch {
while (isActive) {
task()
delay(interval)
}
}
}
The isActive property ensures that the loop stops when the coroutine scope gets canceled, making this approach ideal for tying long-running tasks to the lifecycle of the coroutine.
Now, let’s test that the scheduler can both execute our task and be canceled:
@Test
fun `infinite scheduler stops when scope is canceled`() = runBlocking {
var taskExecutionCount = 0
val task: suspend () -> Unit = {
taskExecutionCount++
}
val schedulerJob = startInfiniteScheduler(interval = 500L, task = task)
delay(1500L)
schedulerJob.cancel()
schedulerJob.join()
assertThat(taskExecutionCount).isCloseTo(3, within(1))
}
The test lets the scheduler repeat a few times before canceling the scope. Then, we check that the number of times the task ran is close to what we expect using AssertJ. We use the isCloseTo() assertion to account for minor variations in delay() timing.
The isActive property, part of the CoroutineScope, flips to false when we cancel the coroutine. This approach suits tasks that need to run indefinitely but must terminate cleanly when their running context becomes invalid.
5. Handling Timeouts in Coroutine-Based Schedulers
In certain situations, it’s important to ensure that a task does not run longer than expected. Kotlin offers the withTimeout() function, which automatically cancels a coroutine if it exceeds the specified time limit. This is useful for tasks that might take longer than expected and should stop after a specific duration.
To make this reusable, the code introduces a helper function to abstract the timeout and task execution logic. This function takes a task (as a callback) and a timeout parameter, allowing the inputs to control the behavior:
suspend fun runWithTimeout(timeout: Long, task: suspend () -> Unit) {
withTimeout(timeout) {
task()
}
}
This function runs a given task with a timeout. If the task exceeds the specified timeout, the system cancels the coroutine and throws a TimeoutCancellationException.
Now, let’s validate that the timeout works as expected:
@Test
fun `runWithTimeout throws TimeoutCancellationException if task exceeds timeout`(): Unit = runBlocking {
val task: suspend () -> Unit = { delay(2000L) }
assertThrows<TimeoutCancellationException> {
runWithTimeout(timeout = 1000L, task = task)
}
}
This test confirms the TimeoutCancellationException by forcing our callback to wait longer than the allowed timeout.
6. Conclusion
Kotlin coroutines provide a flexible and efficient way to implement timers and schedulers. Whether it’s a simple delay() based timer or a complex scheduler for background tasks, Kotlin’s coroutine API makes it easy to handle time-based tasks in a clean and manageable way.
By using coroutine features such as delay(), launch(), and withTimeout(), we can create powerful scheduling mechanisms with clear assertions, ensuring that our schedulers function correctly.
The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.