Implement timer in shared code in Kotlin Multiplatform Mobile

1.3k Views Asked by At

I'm trying to implement a timer function in the shared code of a Kotlin Multiplatform Mobile project. The timer shall run for n seconds, and every second it shall call back to update the UI. Moreover, a button in the UI can cancel the timer. This inevitably means I have to start a new thread of some sort, and my question is which mechanism is the appropriate one to use - workers, coroutines or something else?

I have tried using a coroutine with the following code but run into InvalidMutabilityException on iOS:

class Timer(val updateInterface: (Int) -> Unit) {
    private var timer: Job? = null

    fun start(seconds: Int) {
        timer = CoroutineScope(EmptyCoroutineContext).launch {
            repeat(seconds) {
                updateInterface(it)
                delay(1000)
            }
            updateInterface(seconds)
        }
    }

    fun stop() {
        timer?.cancel()
    }
}

I do know about the moko-time library, but I feel this should be possible without taking on dependencies, and I would like to learn how.

2

There are 2 best solutions below

3
On BEST ANSWER

As you suspect in the comment, updateInterface is a property of the containing class, so capturing a reference to that in the lambda will freeze the parent as well. This is probably the most common and confusing way to freeze your class.

I'd try something like this:

class Timer(val updateInterface: (Int) -> Unit) {
    private var timer: Job? = null

    init {
        ensureNeverFrozen()
    }

    fun start(seconds: Int) {
        val callback = updateInterface
        timer = CoroutineScope(EmptyCoroutineContext).launch {
            repeat(seconds) {
                callback(it)
                delay(1000)
            }
            callback(seconds)
        }
    }

    fun stop() {
        timer?.cancel()
    }
}

It's a little verbose, but make a local val for the callback before capturing it in the lambda.

Also, adding ensureNeverFrozen() will give you a stack trace to the point where the class is frozen rather than later in the call.

For more detail, see https://www.youtube.com/watch?v=oxQ6e1VeH4M&t=1429s and a somewhat simplified blog post series: https://dev.to/touchlab/practical-kotlin-native-concurrency-ac7

0
On

I did a similar thing in one of the tasks, using the extension function for coroutine scope:

fun CoroutineScope.Ticker(
    tickInMillis: Long,
    onTick: () -> Unit
) {
    this.launch(Dispatchers.Default) {
        while (true) {
            withContext(Dispatchers.Main) { onTick() }
            delay(tickInMillis)
        }
    }
}

First implement dispatchers for both platforms, and next call this on a suitable scope.