Cómo implementar el temporizador con las rutinas de Kotlin

8 minutos de lectura

avatar de usuario
Roman Nazarevich

Quiero implementar un temporizador usando corrutinas de Kotlin, algo similar a esto implementado con RxJava:

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

Emitirá LocalDateTime cada nuevo minuto.

  • Creo que puedes usar canales de ticker: kotlinlang.org/docs/reference/coroutines/…

    – marstran

    22 de febrero de 2019 a las 12:51

  • @marstran Ya no, ahora están obsoletos.

    – Farid

    23 de diciembre de 2021 a las 17:36

avatar de usuario
Joffrey

Editar: tenga en cuenta que la API sugerida en la respuesta original ahora está marcada @ObsoleteCoroutineApi:

Los canales de teletipo no están integrados actualmente con la concurrencia estructurada y su API cambiará en el futuro.

Ahora puede utilizar el Flow API para crear tu propio flujo de teletipo:

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

Y puedes usarlo de una manera muy similar a tu código actual:

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

Nota: con el código como está escrito aquí, el tiempo necesario para procesar los elementos no se tiene en cuenta por tickerFlowpor lo que es posible que el retraso no sea regular (es un retraso Entre procesamiento de elementos). Si desea que el ticker marque independientemente del procesamiento de cada elemento, es posible que desee utilizar un buffer o un hilo dedicado (por ejemplo, a través de flowOn).


respuesta original

Creo que todavía es experimental, pero puede usar un TickerChannel para producir valores cada X milis:

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

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

Si necesita continuar con su trabajo mientras su “suscriptor” hace algo por cada “tick”, puede launch una rutina de fondo que leerá desde este canal y hará lo que quieras:

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

Si desea detenerse desde dentro del bucle, simplemente puede salir de él y luego cancelar el canal:

val ticker = ticker(500, 0)

var count = 0

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

ticker.cancel()

  • ¿Hay alguna forma de “anular la cancelación” de un ticker? ¿Cómo puedo pausar/reanudar el ticker?

    – Vidas

    2 de diciembre de 2019 a las 18:45

  • @Lifes, probablemente necesite tener algún tipo de variable de estado “activa” para verificar cuando recibe un tic. Puede establecerlo en falso cuando desee “pausar” y volver a verdadero cuando desee “reanudar”

    – Joffrey

    2 de diciembre de 2019 a las 23:12

  • Gracias por su rápida respuesta. Dado mi caso de uso, no quiero que siga funcionando, así que lo cancelaré y lo recrearé según sea necesario.

    – Vidas

    3 de diciembre de 2019 a las 2:43

  • ticker está marcado como “ObsoleteCoroutinesApi” en la versión “1.3.2”, lo que significa: “Marca las declaraciones que son obsoleto en coroutines API, lo que significa que el diseño de las declaraciones correspondientes tiene fallas graves conocidas y serán rediseñadas en el futuro. En términos generales, estas declaraciones quedarán obsoletas en el futuro, pero aún no hay reemplazo para ellas, por lo que no pueden quedar obsoletas de inmediato”.

    – aLx

    13 de marzo de 2020 a las 14:18


Un enfoque muy pragmático con Kotlin Flows podría ser:

// 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}")
}

  • ¿Cómo ser notificado cuando termina?

    – Skizo-ozᴉʞS

    18 de noviembre de 2021 a las 9:50

  • Asegúrese de importar el flujo usando: import kotlinx.coroutines.flow.collect

    – Juan

    8 de marzo a las 16:53

  • ¿Por qué estamos usando aquí la función asSequence()?

    – Hassa

    29 de junio a las 7:07

  • @Hassa para ser la secuencia de Ints que se crea perezosamente. De lo contrario, todos los Ints desde 0 .. Int.MAX_VALUE se cargarían en la memoria inmediatamente, lo que probablemente no desearía.

    – Steffen Funke

    29 de junio a las 10:05

avatar de usuario
Rafael C.

otra posible solución como una extensión kotlin reutilizable de CoroutineScope

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

y luego el uso como:

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

y luego para interrumpirlo:

job.cancel()

otra nota: consideramos aquí que action no bloquea y no toma tiempo.

  • No importa mucho aquí gracias a la delay() llamar, pero en general debemos evitar while (true) en corrutinas, prefiero while(isActive) para respaldar adecuadamente la cancelación.

    – Joffrey

    1 de diciembre de 2020 a las 17:08

  • @Joffrey, este es solo un ejemplo, siéntase libre de modificarlo para mejorarlo.

    – Rafael C.

    3 de diciembre de 2020 a las 5:53

  • ¿Cuál es la razón para usar async() en vez de launch() ?

    – Phileo99

    5 oct 2021 a las 23:17

  • @Phileo99 Creo que podría hacerlo de cualquier manera, pero si usa Async, devuelve un Deferred que le brinda algunas opciones más que un lanzamiento {}, como await(). No estoy seguro de que sea tan útil en este caso, pero no creo que agregue muchos gastos generales. El diferido extiende el trabajo, por lo que cualquier cosa que el lanzamiento pueda hacer de forma asíncrona también puede hacerlo.

    – AlexW.HB

    1 de febrero a las 19:25

  • Tenga en cuenta que el intervalo entre los siguientes action() las llamadas no son las definidas repeatMillis tiempo, pero repeatMillis + el tiempo que action() tarda en ejecutarse. Así que esta solución está bien siempre y cuando action() no toma mucho tiempo. Usando flujos con buffer(), conflate()o flowOnpodemos obtener intervalos que son casi constantes.

    – Lucas Lechner

    7 de julio a las 8:24

Puedes crear un temporizador de cuenta regresiva como este

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!"
        }

avatar de usuario
Benjamín Ledet

Editar: Joffrey ha editado su solución con un mejor enfoque.

Antiguo :

La solución de Joffrey funciona para mí, pero me encontré con un problema con el bucle for.

Tengo que cancelar mi ticker en el ciclo for de esta manera:

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

Pero ticker.cancel() estaba lanzando una excepción de cancelación porque el ciclo for siguió funcionando después de esto.

Tuve que usar un bucle while para verificar si el canal no estaba cerrado para no obtener esta excepción.

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

  • ¿Por qué no simplemente break fuera del circuito si sabe que quiere que se detenga? Luego puede cancelar el ticker fuera del ciclo, esto funcionó bien para mí. Además, está creando un nuevo iterador en cada giro del bucle con este enfoque, es posible que esto no sea lo que desea hacer.

    – Joffrey

    3 de diciembre de 2019 a las 16:55


  • A veces no pensamos en las soluciones más sencillas… Tienes toda la razón, ¡gracias!

    – Benjamín Ledet

    4 de diciembre de 2019 a las 8:41

  • No hay problema 🙂 Dicho esto, no esperaba cancel() a fallar cuando se le llama desde dentro del bucle, así que me enseñó algo sobre esto. Tendré que investigar más para llegar al fondo de esto.

    – Joffrey

    4 de diciembre de 2019 a las 10:38

  • ¡Bueno, con la versión 1.2.2 de coroutines no falló! Pero actualicé a la versión 1.3.2 y ahora sí. Tal vez se suponía que fallaría con el 1.2.2 y lo arreglaron o es un error introducido …

    – Benjamín Ledet

    4 de diciembre de 2019 a las 11:27

avatar de usuario
Darío Pellegrini

Aquí hay una posible solución usando 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()
    }
}

Uso

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

...

job.cancel()

  • ¿Por qué no simplemente break fuera del circuito si sabe que quiere que se detenga? Luego puede cancelar el ticker fuera del ciclo, esto funcionó bien para mí. Además, está creando un nuevo iterador en cada giro del bucle con este enfoque, es posible que esto no sea lo que desea hacer.

    – Joffrey

    3 de diciembre de 2019 a las 16:55


  • A veces no pensamos en las soluciones más sencillas… Tienes toda la razón, ¡gracias!

    – Benjamín Ledet

    4 de diciembre de 2019 a las 8:41

  • No hay problema 🙂 Dicho esto, no esperaba cancel() a fallar cuando se le llama desde dentro del bucle, así que me enseñó algo sobre esto. Tendré que investigar más para llegar al fondo de esto.

    – Joffrey

    4 de diciembre de 2019 a las 10:38

  • ¡Bueno, con la versión 1.2.2 de coroutines no falló! Pero actualicé a la versión 1.3.2 y ahora sí. Tal vez se suponía que fallaría con el 1.2.2 y lo arreglaron o es un error introducido …

    – Benjamín Ledet

    4 de diciembre de 2019 a las 11:27

Temporizador con funciones START, PAUSE y STOP.

Uso:

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

Timer clase:

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
                    [email protected]
                }
                delay(timeMillis = countDownInterval)
                _tick.value -= countDownInterval
                onTick?.invoke([email protected]_tick.value)
            }
        }
    }

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

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

me inspiré en aquí.

¿Ha sido útil esta solución?