How to implement timer with Kotlin coroutines

Edit: note that the API suggested in the original answer is now marked @ObsoleteCoroutineApi:

Ticker channels are not currently integrated with structured concurrency and their api will change in the future.

You can now use the Flow API to create your own ticker flow:

import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
    delay(initialDelay)
    while (true) {
        emit(Unit)
        delay(period)
    }
}

And you can use it in a way very similar to your current code:

tickerFlow(5.seconds)
    .map { LocalDateTime.now() }
    .distinctUntilChanged { old, new ->
        old.minute == new.minute
    }
    .onEach {
        setDateTime(it)
    }
    .launchIn(viewModelScope) // or lifecycleScope or other

Important note: with the code as written here, the time taken to process elements is not taken into account by tickerFlow, so the delay might not be regular (it’s a delay between element processing). If you want the ticker to tick independently of the processing of each element, you may want to use a buffer or a dedicated thread (e.g. via flowOn).


Original answer

I believe it is still experimental, but you may use a TickerChannel to produce values every X millis:

val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)

repeat(10) {
    tickerChannel.receive()
    val currentTime = LocalDateTime.now()
    println(currentTime)
}

If you need to carry on doing your work while your “subscribe” does something for each “tick”, you may launch a background coroutine that will read from this channel and do the thing you want:

val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)

launch {
    for (event in tickerChannel) {
        // the 'event' variable is of type Unit, so we don't really care about it
        val currentTime = LocalDateTime.now()
        println(currentTime)
    }
}

delay(1000)

// when you're done with the ticker and don't want more events
tickerChannel.cancel()

If you want to stop from inside the loop, you can simply break out of it, and then cancel the channel:

val ticker = ticker(500, 0)

var count = 0

for (event in ticker) {
    count++
    if (count == 4) {
        break
    } else {
        println(count)
    }
}

ticker.cancel()

Leave a Comment