簡體   English   中英

如何使用 Kotlin 協程實現計時器

[英]How to implement timer with Kotlin coroutines

我想使用 Kotlin 協程實現計時器,類似於用 RxJava 實現的東西:

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

它將每隔一分鍾發出 LocalDateTime 。

編輯:請注意,原始答案中建議的 API 現在標記為@ObsoleteCoroutineApi

Ticker 通道目前未與結構化並發集成,它們的 api 將在未來發生變化。

您現在可以使用Flow API 創建您自己的代碼流:

import kotlin.time.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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

您可以以與當前代碼非常相似的方式使用它:

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

如果您不想要實驗性Duration API,您也可以使用Long毫秒。

注意:使用此處編寫的代碼, tickerFlow不考慮處理元素所花費的tickerFlow ,因此延遲可能不正常(這是元素處理之間的延遲)。 如果您希望股票行情獨立於每個元素的處理,您可能需要使用緩沖區或專用線程(例如通過flowOn )。


原答案

我相信它仍然是實驗性的,但是您可以使用TickerChannel每 X 毫秒生成一個值:

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

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

如果您需要在“訂閱”為每個“滴答”做一些事情的同時繼續做您的工作,您可以launch一個后台協程,它將從這個頻道中讀取並做您想做的事情:

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

如果你想從循環內部停止,你可以簡單地跳出循環,然后取消通道:

val ticker = ticker(500, 0)

var count = 0

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

ticker.cancel()

Kotlin Flows 的一種非常務實的方法可能是:

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

您可以像這樣創建倒數計時器

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

另一個可能的解決方案作為可重復使用的擴展科特林CoroutineScope

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

然后用作:

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

然后打斷它:

job.cancel()

編輯:喬弗里用更好的方法編輯了他的解決方案。

老的 :

Joffrey的解決方案對我有用,但我遇到了 for 循環問題。

我必須在 for 循環中取消我的自動收報機,如下所示:

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

但是ticker.cancel()拋出了一個取消ticker.cancel()因為 for 循環在此之后繼續運行。

我不得不使用 while 循環來檢查通道是否未關閉以防止出現此異常。

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

這是使用 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()
    }
}

用法

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

...

job.cancel()

這是基於 Joffrey 回答的Observable.intervalRange(1, 5, 0, 1, TimeUnit.SECONDS)Flow版本:

fun tickerFlow(start: Long,
               count: Long,
               initialDelayMs: Long,
               periodMs: Long) = flow<Long> {
    delay(initialDelayMs)

    var counter = start
    while (counter <= count) {
        emit(counter)
        counter += 1

        delay(periodMs)
    }
}

//...

tickerFlow(1, 5, 0, 1_000L)

制作Observable.intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)的副本Observable.intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)每 1 秒將在 90 秒內發出項目):

fun intervalRange(start: Long, count: Long, initialDelay: Long = 0, period: Long, unit: TimeUnit): Flow<Long> {
        return flow<Long> {
            require(count >= 0) { "count >= 0 required but it was $count" }
            require(initialDelay >= 0) { "initialDelay >= 0 required but it was $initialDelay" }
            require(period > 0) { "period > 0 required but it was $period" }

            val end = start + (count - 1)
            require(!(start > 0 && end < 0)) { "Overflow! start + count is bigger than Long.MAX_VALUE" }

            if (initialDelay > 0) {
                delay(unit.toMillis(initialDelay))
            }

            var counter = start
            while (counter <= count) {
                emit(counter)
                counter += 1

                delay(unit.toMillis(period))
            }
        }
    }

用法:

lifecycleScope.launch {
intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)
                .onEach {
                    Log.d(TAG, "intervalRange: ${90 - it}")
                }
                .lastOrNull()
}

具有啟動、暫停和停止功能的定時器。

class TimerViewModel : ViewModel() {
    private var job: Job? = null
    private val _times = MutableStateFlow(0)
    val times = _times.asStateFlow()

    fun start(times: Int = 60) {
        if (_times.value == 0) _times.value = times
        job?.cancel()
        job = viewModelScope.launch(Dispatchers.IO) {
            while (isActive) {
                if (_times.value <= 0) {
                    job?.cancel()
                    return@launch
                }
                delay(timeMillis = 1000)
                _times.value -= 1
            }
        }
    }

    fun pause() {
        job?.cancel()
    }

    fun stop() {
        job?.cancel()
        _times.value = 0
    }
} 

我在這里舉了這個例子。

在此處輸入圖像描述

enter code here
private val updateLiveShowTicker = flow {
    while (true) {
        emit(Unit)
        delay(1000L * UPDATE_PROGRAM_INFO_INTERVAL_SECONDS)
    }
}

private val updateShowProgressTicker = flow {
    while (true) {
        emit(Unit)
        delay(1000L * UPDATE_SHOW_PROGRESS_INTERVAL_SECONDS)
    }
}

private val liveShow = updateLiveShowTicker
    .combine(channelId) { _, channelId -> programInfoRepository.getShow(channelId) }
    .catch { emit(LiveShow(application.getString(R.string.activity_channel_detail_info_error))) }
    .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), replay = 1)
    .distinctUntilChanged()

我的解決方案,您現在可以使用 Flow API 創建自己的代碼流:

最近使用它來根據計時器和最大緩沖區大小來分塊值。

private object Tick

@Suppress("UNCHECKED_CAST")
fun <T : Any> Flow<T>.chunked(size: Int, initialDelay: Long, delay: Long): Flow<List<T>> = flow {
    if (size <= 0) throw IllegalArgumentException("invalid chunk size $size - expected > 0")
    val chunkedList = mutableListOf<T>()
    if (delay > 0L) {
        merge(this@chunked, timerFlow(initialDelay, delay, Tick))
    } else {
        this@chunked
    }
        .collect {
            when (it) {
                is Tick -> {
                    if (chunkedList.isNotEmpty()) {
                        emit(chunkedList.toList())
                        chunkedList.clear()
                    }
                }
                else -> {
                    chunkedList.add(it as T)
                    if (chunkedList.size >= size) {
                        emit(chunkedList.toList())
                        chunkedList.clear()
                    }
                }
            }
        }
    if (chunkedList.isNotEmpty()) {
        emit(chunkedList.toList())
    }
}

fun <T> timerFlow(initialDelay: Long, delay: Long, o: T) = flow {
    if (delay <= 0) throw IllegalArgumentException("invalid delay $delay - expected > 0")
    if (initialDelay > 0) delay(initialDelay)
    while (currentCoroutineContext().isActive) {
        emit(o)
        delay(delay)
    }
}

它沒有使用 Kotlin 協程,但是如果您的用例足夠簡單,您總是可以使用諸如fixedRateTimertimer此處文檔)之類的東西,它們解析為 JVM 本機Timer

我在一個相對簡單的場景中使用了 RxJava 的interval ,當我切換到使用 Timers 時,我看到了顯着的性能和內存改進。

您還可以使用View.post()或其多個變體在 Android 的主線程上運行您的代碼。

唯一真正的煩惱是您需要自己跟蹤舊時代的狀態,而不是依靠 RxJava 來為您完成。

但這總是要快得多(如果你正在做 UI 動畫等性能關鍵的東西,這很重要)並且不會有 RxJava 的 Flowables 的內存開銷。

這是使用fixedRateTimer的問題代碼:


var currentTime: LocalDateTime = LocalDateTime.now()

fixedRateTimer(period = 5000L) {
    val newTime = LocalDateTime.now()
    if (currentTime.minute != newTime.minute) {
        post { // post the below code to the UI thread to update UI stuff
            setDateTime(newTime)
        }
        currentTime = newTime
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM