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.
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