So implementieren Sie Timer mit Kotlin-Coroutinen

Lesezeit: 8 Minuten

Benutzeravatar von Roman Nazarevych
Roman Nasarewitsch

Ich möchte Timer mit Kotlin-Coroutinen implementieren, ähnlich wie bei RxJava:

       Flowable.interval(0, 5, TimeUnit.SECONDS)
                    .observeOn(AndroidSchedulers.mainThread())
                    .map { LocalDateTime.now() }
                    .distinctUntilChanged { old, new ->
                        old.minute == new.minute
                    }
                    .subscribe {
                        setDateTime(it)
                    }

Es wird LocalDateTime jede neue Minute ausgeben.

  • Ich denke, Sie können Tickerkanäle verwenden: kotlinlang.org/docs/reference/coroutines/…

    – Marsran

    22. Februar 2019 um 12:51 Uhr

  • @marstran Nicht mehr, sie sind jetzt veraltet.

    – Farid

    23. Dezember 2021 um 17:36 Uhr

Benutzeravatar von Joffrey
Joffrey

Bearbeiten: Beachten Sie, dass die in der ursprĂĽnglichen Antwort vorgeschlagene API jetzt markiert ist @ObsoleteCoroutineApi:

Tickerkanäle sind derzeit nicht in die strukturierte Parallelität integriert und ihre API wird sich in Zukunft ändern.

Sie können jetzt die verwenden Flow API zum Erstellen Ihres eigenen Ticker-Flows:

import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun tickerFlow(period: Duration, initialDelay: Duration = Duration.ZERO) = flow {
    delay(initialDelay)
    while (true) {
        emit(Unit)
        delay(period)
    }
}

Und Sie können es auf eine Weise verwenden, die Ihrem aktuellen Code sehr ähnlich ist:

tickerFlow(5.seconds)
    .map { LocalDateTime.now() }
    .distinctUntilChanged { old, new ->
        old.minute == new.minute
    }
    .onEach {
        setDateTime(it)
    }
    .launchIn(viewModelScope) // or lifecycleScope or other

Hinweis: Bei dem hier geschriebenen Code wird die Zeit, die zum Verarbeiten von Elementen benötigt wird, nicht berücksichtigt tickerFlowdaher ist die Verzögerung möglicherweise nicht regelmäßig (es ist eine Verzögerung zwischen Elementverarbeitung). Wenn Sie möchten, dass der Ticker unabhängig von der Verarbeitung jedes Elements tickt, können Sie a verwenden Puffer oder einen eigenen Thread (zB via flowOn).


UrsprĂĽngliche Antwort

Ich glaube, es ist noch experimentell, aber Sie können a verwenden TickerChannel um Werte alle X Millis zu erzeugen:

val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)

repeat(10) {
    tickerChannel.receive()
    val currentTime = LocalDateTime.now()
    println(currentTime)
}

Wenn Sie Ihre Arbeit fortsetzen müssen, während Ihr “Abonnement” für jeden “Tick” etwas tut, können Sie das tun launch eine Hintergrund-Coroutine, die von diesem Kanal liest und das tut, was Sie wollen:

val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)

launch {
    for (event in tickerChannel) {
        // the 'event' variable is of type Unit, so we don't really care about it
        val currentTime = LocalDateTime.now()
        println(currentTime)
    }
}

delay(1000)

// when you're done with the ticker and don't want more events
tickerChannel.cancel()

Wenn Sie innerhalb der Schleife anhalten möchten, können Sie einfach ausbrechen und dann den Kanal abbrechen:

val ticker = ticker(500, 0)

var count = 0

for (event in ticker) {
    count++
    if (count == 4) {
        break
    } else {
        println(count)
    }
}

ticker.cancel()

  • Gibt es eine Möglichkeit, einen Ticker zu “unstornieren”? Wie kann ich den Ticker pausieren/aufheben?

    – Leben

    2. Dezember 2019 um 18:45 Uhr

  • @Lifes Sie mĂĽssen wahrscheinlich eine Art “aktive” Zustandsvariable haben, um zu ĂĽberprĂĽfen, wann Sie ein Tick erhalten. Sie können es auf “false” setzen, wenn Sie “pausieren” möchten, und wieder auf “true”, wenn Sie “fortsetzen” möchten.

    – Joffrey

    2. Dezember 2019 um 23:12 Uhr

  • Danke fĂĽr die schnelle Antwort. Angesichts meines Anwendungsfalls möchte ich nicht, dass es weiter tickt, also werde ich es nach Bedarf abbrechen und neu erstellen.

    – Leben

    3. Dezember 2019 um 2:43 Uhr

  • Ticker ist in Version “1.3.2” als “ObsoleteCoroutinesApi” gekennzeichnet, was bedeutet: “Markiert Deklarationen, die obsolet in Coroutines API, was bedeutet, dass das Design der entsprechenden Deklarationen schwerwiegende bekannte Fehler aufweist und sie in Zukunft neu gestaltet werden. Grob gesagt werden diese Deklarationen in Zukunft veraltet sein, aber es gibt noch keinen Ersatz fĂĽr sie, sodass sie nicht sofort veraltet sein können.”

    – aLx

    13. März 2020 um 14:18 Uhr


Ein sehr pragmatischer Ansatz mit Kotlin Flows könnte sein:

// Create the timer flow
val timer = (0..Int.MAX_VALUE)
    .asSequence()
    .asFlow()
    .onEach { delay(1_000) } // specify delay

// Consume it
timer.collect { 
    println("bling: ${it}")
}

  • Wie werde ich benachrichtigt, wenn es endet?

    – Skizo-ozᴉʞS

    18. November 2021 um 9:50 Uhr

  • Achten Sie darauf, den Flow zu importieren mit: import kotlinx.coroutines.flow.collect

    – Johannes

    8. März um 16:53 Uhr

  • Warum verwenden wir hier die Funktion asSequence()?

    – Hassa

    29. Juni um 7:07 Uhr

  • @Hassa soll die Folge von Ints sein, die faul erstellt werden. Andernfalls wĂĽrden alle Ints von 0 .. Int.MAX_VALUE sofort in den Speicher geladen, was Sie wahrscheinlich nicht möchten.

    – Steffen Funke

    29. Juni um 10:05 Uhr

Benutzeravatar von Raphael C
Raffael C

eine weitere mögliche Lösung als wiederverwendbare Kotlin-Erweiterung von CoroutineScope

fun CoroutineScope.launchPeriodicAsync(
    repeatMillis: Long,
    action: () -> Unit
) = this.async {
    if (repeatMillis > 0) {
        while (isActive) {
            action()
            delay(repeatMillis)
        }
    } else {
        action()
    }
}

und dann Verwendung als:

var job = CoroutineScope(Dispatchers.IO).launchPeriodicAsync(100) {
  //...
}

und um es dann zu unterbrechen:

job.cancel()

noch eine Anmerkung: wir betrachten das hier action ist nicht blockierend und nimmt keine Zeit in Anspruch.

  • Dank dem spielt es hier keine Rolle delay() Anruf, aber im Allgemeinen sollten wir vermeiden while (true) in Koroutinen bevorzugen while(isActive) Stornierung richtig zu unterstĂĽtzen.

    – Joffrey

    1. Dezember 2020 um 17:08 Uhr

  • @Joffrey, dies ist nur ein Beispiel, Sie können es gerne zum Besseren ändern.

    – Raffael C

    3. Dezember 2020 um 5:53 Uhr

  • Was ist der Grund fĂĽr die Verwendung async() Anstatt von launch() ?

    – Phileo99

    5. Oktober 2021 um 23:17 Uhr

  • @Phileo99 Ich denke, Sie könnten es so oder so tun, aber wenn Sie Async verwenden, wird ein Deferred zurĂĽckgegeben, das Ihnen ein paar mehr Optionen als ein Start {} bietet, z. B. await(). Ich bin mir nicht sicher, ob das in diesem Fall so nĂĽtzlich wäre, aber ich glaube nicht, dass es viel Overhead hinzufĂĽgt. Verzögert erweitert Job, sodass alles, was der Start asynchron ausfĂĽhren kann, auch ausgefĂĽhrt werden kann.

    – AlexW.HB

    1. Februar um 19:25 Uhr

  • Denken Sie daran, dass das Intervall zwischen aufeinander folgenden action() Anrufe ist nicht definiert repeatMillis Zeit, aber repeatMillis + die Zeit, dass action() dauert zur AusfĂĽhrung. Diese Lösung ist also in Ordnung, solange action() dauert nicht zu lange. Durch die Verwendung von FlĂĽssen mit buffer(), conflate()oder flowOnkönnen wir Intervalle erhalten, die ungefähr konstant sind.

    – Lukas Lechner

    7. Juli um 8:24 Uhr

Sie können einen Countdown-Timer wie diesen erstellen

GlobalScope.launch(Dispatchers.Main) {
            val totalSeconds = TimeUnit.MINUTES.toSeconds(2)
            val tickSeconds = 1
            for (second in totalSeconds downTo tickSeconds) {
                val time = String.format("%02d:%02d",
                    TimeUnit.SECONDS.toMinutes(second),
                    second - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(second))
                )
                timerTextView?.text = time
                delay(1000)
            }
            timerTextView?.text = "Done!"
        }

Benutzeravatar von Benjamin Ledet
Benjamin Ledet

Bearbeiten: Joffrey hat seine Lösung mit einem besseren Ansatz bearbeitet.

Alt :

Joffreys Lösung funktioniert für mich, aber ich bin auf ein Problem mit der for-Schleife gestoßen.

Ich muss meinen Ticker in der for-Schleife wie folgt löschen:

            val ticker = ticker(500, 0)
            for (event in ticker) {
                if (...) {
                    ticker.cancel()
                } else {
                    ...
                    }
                }
            }

Aber ticker.cancel() hat eine CancellationException ausgelöst, weil die for-Schleife danach weiterging.

Ich musste eine While-Schleife verwenden, um zu ĂĽberprĂĽfen, ob der Kanal nicht geschlossen wurde, um diese Ausnahme nicht zu erhalten.

                val ticker = ticker(500, 0)
                while (!ticker.isClosedForReceive && ticker.iterator().hasNext()) {
                    if (...) {
                        ticker.cancel()
                    } else {
                        ...
                        }
                    }
                }

  • Warum gehst du nicht einfach break aus der Schleife, wenn Sie wissen, dass Sie wollen, dass es aufhört? Sie können den Ticker dann auĂźerhalb der Schleife abbrechen, das hat bei mir gut funktioniert. AuĂźerdem erstellen Sie mit diesem Ansatz bei jeder Schleifenumdrehung einen neuen Iterator. Dies ist möglicherweise nicht das, was Sie tun möchten.

    – Joffrey

    3. Dezember 2019 um 16:55 Uhr


  • Manchmal denken wir nicht an die einfachsten Lösungen … Sie haben vollkommen recht, danke!

    – Benjamin Ledet

    4. Dezember 2019 um 8:41 Uhr

  • Kein Problem 🙂 Davon abgesehen, hatte ich nicht erwartet cancel() zu scheitern, wenn es innerhalb der Schleife aufgerufen wird, also haben Sie mir etwas dazu beigebracht. Um dem auf den Grund zu gehen, muss ich weiter recherchieren.

    – Joffrey

    4. Dezember 2019 um 10:38 Uhr

  • Nun, mit der Version 1.2.2 der Coroutinen ist es nicht fehlgeschlagen! Aber ich habe auf die Version 1.3.2 aktualisiert und jetzt funktioniert es. Vielleicht sollte es mit 1.2.2 fehlschlagen und sie haben es behoben oder es ist ein Fehler, der eingefĂĽhrt wurde …

    – Benjamin Ledet

    4. Dezember 2019 um 11:27 Uhr

Benutzeravatar von Dario Pellegrini
Dario Pellegrini

Hier ist eine mögliche Lösung mit Kotlin Flow

fun tickFlow(millis: Long) = callbackFlow<Int> {
    val timer = Timer()
    var time = 0
    timer.scheduleAtFixedRate(
        object : TimerTask() {
            override fun run() {
                try { offer(time) } catch (e: Exception) {}
                time += 1
            }
        },
        0,
        millis)
    awaitClose {
        timer.cancel()
    }
}

Verwendungszweck

val job = CoroutineScope(Dispatchers.Main).launch {
   tickFlow(125L).collect {
      print(it)
   }
}

...

job.cancel()

  • Warum gehst du nicht einfach break aus der Schleife, wenn Sie wissen, dass Sie wollen, dass es aufhört? Sie können den Ticker dann auĂźerhalb der Schleife abbrechen, das hat bei mir gut funktioniert. AuĂźerdem erstellen Sie mit diesem Ansatz bei jeder Schleifenumdrehung einen neuen Iterator. Dies ist möglicherweise nicht das, was Sie tun möchten.

    – Joffrey

    3. Dezember 2019 um 16:55 Uhr


  • Manchmal denken wir nicht an die einfachsten Lösungen … Sie haben vollkommen recht, danke!

    – Benjamin Ledet

    4. Dezember 2019 um 8:41 Uhr

  • Kein Problem 🙂 Davon abgesehen, hatte ich nicht erwartet cancel() zu scheitern, wenn es innerhalb der Schleife aufgerufen wird, also haben Sie mir etwas dazu beigebracht. Um dem auf den Grund zu gehen, muss ich weiter recherchieren.

    – Joffrey

    4. Dezember 2019 um 10:38 Uhr

  • Nun, mit der Version 1.2.2 der Coroutinen ist es nicht fehlgeschlagen! Aber ich habe auf die Version 1.3.2 aktualisiert und jetzt funktioniert es. Vielleicht sollte es mit 1.2.2 fehlschlagen und sie haben es behoben oder es ist ein Fehler, der eingefĂĽhrt wurde …

    – Benjamin Ledet

    4. Dezember 2019 um 11:27 Uhr

Timer mit START-, PAUSE- und STOP-Funktion.

Verwendungszweck:

val timer = Timer(millisInFuture = 10_000L, runAtStart = false)
timer.start()

Timer Klasse:

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

enum class PlayerMode {
    PLAYING,
    PAUSED,
    STOPPED
}

class Timer(
    val millisInFuture: Long,
    val countDownInterval: Long = 1000L,
    runAtStart: Boolean = false,
    val onFinish: (() -> Unit)? = null,
    val onTick: ((Long) -> Unit)? = null
) {
    private var job: Job = Job()
    private val _tick = MutableStateFlow(0L)
    val tick = _tick.asStateFlow()
    private val _playerMode = MutableStateFlow(PlayerMode.STOPPED)
    val playerMode = _playerMode.asStateFlow()

    private val scope = CoroutineScope(Dispatchers.Default)

    init {
        if (runAtStart) start()
    }

    fun start() {
        if (_tick.value == 0L) _tick.value = millisInFuture
        job.cancel()
        job = scope.launch(Dispatchers.IO) {
            _playerMode.value = PlayerMode.PLAYING
            while (isActive) {
                if (_tick.value <= 0) {
                    job.cancel()
                    onFinish?.invoke()
                    _playerMode.value = PlayerMode.STOPPED
                    return@launch
                }
                delay(timeMillis = countDownInterval)
                _tick.value -= countDownInterval
                onTick?.invoke(this@Timer._tick.value)
            }
        }
    }

    fun pause() {
        job.cancel()
        _playerMode.value = PlayerMode.PAUSED
    }

    fun stop() {
        job.cancel()
        _tick.value = 0
        _playerMode.value = PlayerMode.STOPPED
    }
}

Ich habe mich inspirieren lassen hier.

  • Warum wechseln Sie zu Dispatcher io?

    – Arkah

    11. September um 3:13

1394690cookie-checkSo implementieren Sie Timer mit Kotlin-Coroutinen

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy