Java's ScheduledExecutorService has a nifty scheduleAtFixedRate API, which allows you to schedule a Runnable to be executed in a recurring manner.

Mimicking scheduleAtFixedRate seems easy, after all, isn't it just a while (true) loop that invokes your task every once in a while?

GlobalScope.launch {
    while (true) {
        println("Hello World!! Loritta is so cute :3")
        delay(5_000)
    }
}

And it seems to work fine, it does print Hello World!! Loritta is so cute :3 every 5s! And you may be wondering: What's wrong with it?

What you implemented is a scheduleAtFixedDelay-like API!

Let's change your code a bit, so we can see the problem in your implementation.

GlobalScope.launch {
    while (true) {
        println("Hello World!! Loritta is so cute :3")
        Thread.sleep(5_000) // This is a blocking call
        delay(5_000)
    }
}

The output will be:

00:00: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3
00:20: Hello World!! Loritta is so cute :3

This is not how scheduleAtFixedRate works, this is how scheduleAtFixedDelay works! Rate schedules the next execution based on the start of the current task, while Delay schedules the next execution based on the end of the current task. You can learn more about this in this StackOverflow answer that I always come back when I need to refresh my memory about what's the difference between the two.

A quick and dirty solution is launching a new coroutine instead of blocking the current coroutine.

GlobalScope.launch {
    while (true) {
        GlobalScope.launch {
            println("Hello World!! Loritta is so cute :3")
            Thread.sleep(5_000) // This is a blocking call
        }
        delay(5_000)
    }
}

The output will be:

00:00: Hello World!! Loritta is so cute :3
00:05: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3

It fixed our issue, yay!! However, let's change your code once again.

GlobalScope.launch {
    while (true) {
        GlobalScope.launch {
            println("Hello World!! Loritta is so cute :3")
            Thread.sleep(15_000) // This is a blocking call
        }
        delay(5_000)
    }
}

The output will be:

00:00: Hello World!! Loritta is so cute :3
00:05: Hello World!! Loritta is so cute :3
00:10: Hello World!! Loritta is so cute :3

And if we look at scheduleAtFixedRate's docs...

If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.

But it is being executed concurrently! So this doesn't work like scheduleAtFixedRate's, but it sure does get close to it!

However, we are getting closer! Let's think... "but will not concurrently execute"... What can block concurrent executions...?

Wait, I know! Mutex!

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration

/**
 * Schedules [action] to be executed on [scope] every [period] with a [initialDelay]
 */
fun scheduleCoroutineAtFixedRate(scope: CoroutineScope, period: Duration, initialDelay: Duration = Duration.ZERO, action: RunnableCoroutine) {
    scope.launch {
        delay(initialDelay)

        val mutex = Mutex()

        while (true) {
            launch {
                mutex.withLock {
                    action.run()
                }
            }
            delay(period)
        }
    }
}

fun interface RunnableCoroutine {
    suspend fun run()
}

Because the RunnableCoroutine is a functional interface, you can call it by passing a block to it.

val scope = CoroutineScope(Dispatchers.Default)

scheduleCoroutineAtFixedRate(scope, 5.seconds) {
    println("Hello World!! Loritta is so cute :3")
}

Or you can pass an instance of a class/object.

val scope = CoroutineScope(Dispatchers.Default)

scheduleCoroutineAtFixedRate(scope, 5.seconds, PantufaIsCuteRunnable())

class PantufaIsCuteRunnable : RunnableCoroutine {
    override suspend fun run() {
        println("Hello World!! Pantufa is so cute :3")
    }
}

Have fun!