Search code examples
kotlin-nativekotlin-multiplatform-mobile

Implement timer in shared code in Kotlin Multiplatform Mobile


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.


Solution

  • 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