Cannot access fixedRateTimer in Kotlin Multiplatform

1.1k Views Asked by At

I'm working on a Kotlin Multiplatform project. And I'm trying to use a timer and countdown timer but I cannot access kotlin.concurrent.fixedRateTimer or import kotlin.concurrent.timer in commonMain module.

enter image description here

However the kotlin.concurrent is available: enter image description here

This is root build.gradle :

plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("kotlin-android-extensions")
}

// ...

kotlin {
    //...
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.10")
                implementation("org.jetbrains.kotlin:kotlin-reflect:1.4.10")
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
                //...
            }
        }
        //...
    }
}

I wonder if it's even possible to use these methods there. If not, how can I write a timer and countdown timer in commonMain module?

I have tried to use Coroutines to achieve the same functionality but failed because they are not precise:

    fun doAfter(delay: Long, action: () -> (Unit)) = launch {
        delay(delay)
        action.invoke()
    }

    fun countdown(time: Long, tick: Long, onTick: () -> (Unit), onFinish: () -> (Unit)) = launch {
        val ticks = (time / tick).toInt()
        repeat(ticks) {
            onTick()
            delay(tick)
        }
        onFinish()
    }
3

There are 3 best solutions below

0
On BEST ANSWER

As said by Qaz, the function you are trying to use in common code is JVM only.

Usually in KMP, when you still don't have a common functionality already built in by the framework you could follow different approaches:

  1. Use someone else library (e.g. moko-time) - Best place for searching libraries is here.
  2. Use the native framworks classes by the expect/actual mechanism

Just to give you and example of what could be done (not sure if that is right for you or could fit your needs. It's just to put you in the right direction and above all what I've wrote could not be production ready at all [-;)

commonMain:Timer.kt

expect class KMMTimer(
    name: String? = null,
    interval: Long,
    delay: Long,
    action: () -> Unit
) {
    val name: String?
    val interval: Long
    val delay: Long

    fun start()
    fun cancel()
    fun isRunning(): Boolean
}

androidMain:Timer.kt

import java.util.*
import kotlin.concurrent.fixedRateTimer

actual class KMMTimer actual constructor(
    actual val name: String?,
    actual val interval: Long,
    actual val delay: Long,
    action: () -> Unit
) {
    private var timer: Timer? = null
    private val action = action

    actual fun start() {
        if (!isRunning()) {
            timer = fixedRateTimer(
                name = name,
                initialDelay = delay,
                period = interval
            ) {
                action()
            }
        }
    }

    actual fun cancel() {
        timer?.cancel()
        timer = null
    }

    actual fun isRunning(): Boolean {
        return timer != null
    }
}

iosMain:Timer.kt

import platform.Foundation.NSDate
import platform.Foundation.NSRunLoop
import platform.Foundation.NSRunLoopCommonModes
import platform.Foundation.NSTimer

actual class KMMTimer actual constructor(
    actual val name: String?,
    actual val interval: Long,
    actual val delay: Long,
    action: () -> Unit
) {
    private var timer: NSTimer? = null
    private var action = action

    actual fun start() {
        if (!isRunning()) {
            timer = NSTimer(
                fireDate = NSDate(
                    NSDate().timeIntervalSinceReferenceDate + (delay.toDouble() / 1000)
                ),
                interval = (interval.toDouble() / 1000),
                repeats = true,
                block = {
                    action()
                }
            )
            timer?.let {
                NSRunLoop.currentRunLoop().addTimer(it, NSRunLoopCommonModes)
            }
        }
    }

    actual fun cancel() {
        timer?.invalidate()
        timer = null
    }

    actual fun isRunning(): Boolean {
        return timer != null
    }
}
0
On

Actually @shadowsheep's answer is great. But for me there is one caveat with ios NSTimer block action that was never fired. The problem was in NSRunLoop.currentRunLoop. The solution is:

        timer?.let {
            NSRunLoop.mainRunLoop.addTimer(it, NSRunLoopCommonModes)
        }
1
On