How to create a countdown with flow coroutines

4.4k Views Asked by At

I'm trying to create a flow with coroutines but it's not giving to me the expected result. What I'd like to have is giving an expiration time (doesn't matter if it's in millis, seconds, etc..) when the time arrives to 0 it stops the countdown. What I have now is :

private fun tickerFlow(start: Long, end: Long = 0L) = flow {
        var count = start
        while (count >= end) {
            emit(Unit)
            count--
            delay(1_000L)
        }
    }

And then I call this function as :

val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
                tickerFlow(expireDate)
                    .map { LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - expireDate }
                    .distinctUntilChanged { old, new ->
                        old == new
                    }
                    .onEach {
                        //Here I should print the timer going down with this pattern 
                        //00h: 00m: 00s I did it with String.format("%02dh: %02dm: %02ds") and it works though.
                    }
                    .onCompletion {
                        //Setting the text when completed
                    }
                    .launchIn(scope = scope)

But even with this test that what I'm trying is to have the expiry time as 10 seconds from now it doesn't print nor end as I would. Am I missing something? Is there any way I could emit the local date time so I have the hours, minutes and seconds? perhaps I have to do the calculus to get the seconds, minutes hours from milis / seconds.

TLDR;

I'm getting from backend an expiry date, and I want to know when this expiry date finish so I have to calculate it with the now() and check when it is expired.

3

There are 3 best solutions below

9
Arpit Shukla On BEST ANSWER

You don't really need a flow here. Try this code:

val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
for (i in (expireDate - currentTime)  downTo 1) {
    println("$i seconds remaining") // Format the remaining seconds however you wish
    delay(1_000) // Delay for 1 second
}
println("TIME UP") // Run your completion code here

Also, this code is safe to run on main thread as delay doesn't block.

In your code, the problem is that you are passing the expireDate itself to tickerFlow. expireDate contains the time in seconds from epoch and not the seconds difference from current time. Just pass expireDate - LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) to tickerFlow and it will work.

EDIT: Complete implementation using flow

private fun tickerFlow(start: Long, end: Long = 0L) = flow {
    for (i in start downTo end) {
        emit(i)
        delay(1_000)
    }
}
val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
    .onEach { secondsRemaining ->
        // Format and display the time
    }
    .onCompletion { 
        // Handle completion
    }
    .launchIn(scope)
0
Skizo-ozᴉʞS ツ On

Solution with flow

private fun countDownFlow(
        start: Long,
        delayInSeconds: Long = 1_000L,
    ) = flow {
        var count = start
        while (count >= 0L) {
            emit(count--)
            delay(delayInSeconds)
        }
    }

And then given an expiration date, get the current date subtract them and pass it as start.

val expireDate = LocalDateTime.now().plusSeconds(10L).toEpochSecond(ZoneOffset.UTC)
val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)
tickerFlow(expireDate - currentTime)
                    .onEach {
                        binding.yourTimer.text = String.format(
                            "%02dh: %02dm: %02ds",
                            TimeUnit.SECONDS.toHours(it),
                            TimeUnit.SECONDS.toMinutes(it),
                            TimeUnit.SECONDS.toSeconds(it),
                        )
                    }
                    .onCompletion {
                        //Update UI
                    }
                    .launchIn(coroutineScope)

0
Darko Martinović On

If in hurry copy/paste this!


     fun getFlow( delayTimeMilliseconds: Long,  startValue: Long,  stepValue : Long = 1,  endValue : Long =0): Flow<Long> =
            ( startValue downTo  endValue step  stepValue ).asFlow().flowOn(Dispatchers.IO)
                    .onEach { delay( delayTimeMilliseconds) } 
                    .onStart { emit( startValue) } 
                    .conflate()
                    .transform { remainingValue: Long ->
                        if(remainingValue<0) emit(0)
                        else emit( remainingValue)
                    }

On my production app I use timer as an UseCase as per clean architecture. Like this :

class TimerFlowUseCase constructor() : StateFlowUseCase<TimerFlowUseCase.Params, Long>() {

    override suspend fun getFlow(params: Params): Flow<Long> =
            (params.startValue downTo params.endValue step params.stepValue ).asFlow().flowOn(Dispatchers.IO)
                    .onEach { delay(params.delayTimeMilliseconds) } 
                    .onStart { emit(params.startValue) } // Emits total value on start
                    .conflate()  
                    .transform { remainingValue: Long ->
                        if(remainingValue<0) emit(0)
                        else emit( remainingValue)
                    }

    data class Params( val delayTimeMilliseconds: Long, val startValue: Long, val stepValue : Long = 1, val endValue : Long =0)
}

Where superclass is:

abstract class StateFlowUseCase<P, R> {
    suspend operator fun invoke(params: P , coroutineScope: CoroutineScope): StateFlow<R> {
        return getFlow(params).stateIn(coroutineScope)
    }
    abstract suspend fun getFlow(params: P): Flow<R>
}