Реализуйте таймер в общем коде в Kotlin Multiplatform Mobile

Я пытаюсь реализовать функцию таймера в общем коде проекта Kotlin Multiplatform Mobile. Таймер должен работать в течение n секунд, и каждую секунду он должен перезванивать для обновления пользовательского интерфейса. Более того, кнопка в пользовательском интерфейсе может отменить таймер. Это неизбежно означает, что я должен начать какой-то новый поток, и мой вопрос в том, какой механизм лучше использовать - рабочие, сопрограммы или что-то еще?

Я пробовал использовать сопрограмму со следующим кодом, но столкнулся с InvalidMutabilityException на 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()
    }
}

Я знаю о библиотеке moko-time, но я считаю, что это должно быть возможно без зависимостей, и я хотел бы узнать, как это сделать.

2 ответа

Как вы подозреваете в комментарии, updateInterfaceявляется свойством содержащего класса, поэтому захват ссылки на него в лямбде также заморозит родительский объект. Это, вероятно, самый распространенный и запутанный способ заморозить ваш класс.

Я бы попробовал что-то вроде этого:

      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()
    }
}

Это немного многословно, но сделайте локальный val для обратного вызова, прежде чем записывать его в лямбда.

Кроме того, добавив ensureNeverFrozen() предоставит вам трассировку стека до точки, в которой класс заморожен, а не позже в вызове.

Подробнее см. Https://www.youtube.com/watch?v=oxQ6e1VeH4M&t=1429s и несколько упрощенную серию сообщений в блоге: https://dev.to/touchlab/practical-kotlin-native-concurrency-ac7

Я проделал то же самое в одной из задач, используя функцию расширения для области сопрограммы:

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

Сначала реализуйте диспетчеров для обеих платформ, а затем вызовите это в подходящей области.

Другие вопросы по тегам