I am experimenting with CountDownTimer in jetpack compose with the following code
#Composable
fun Timer() {
val millisInFuture: Long = 10 * 1000 // TODO: get actual value
val timeData = remember {
mutableStateOf(millisInFuture)
}
val countDownTimer =
object : CountDownTimer(millisInFuture, 1000) {
override fun onTick(millisUntilFinished: Long) {
Log.d("TAG", "onTick: ")
timeData.value = millisInFuture
}
override fun onFinish() {
}
}
DisposableEffect(key1 = "key") {
countDownTimer.start()
onDispose {
countDownTimer.cancel()
}
}
Text(
text = timeData.value.toString()
)
}
In the logcat I am able to see the timer is ticking but the UI is not updating .
Please explain why there is on recomposition on changing the value of state variable.
Well, Within the CountDownTimer, instead of setting millisInFuture, you should set millisUntilFinished. That variable holds the updated value, the millisInFuture never changes
timeData.value = millisUntilFinished
You can try this code to implement a Countdown timer:
val time = (timerDate.time).minus(Calendar.getInstance().timeInMillis)
var timer by remember { mutableStateOf(time) }
LaunchedEffect(key1 = timer) {
if (timer > 0) {
delay(1000L)
timer -= 1000L
}
}
val secMilSec: Long = 1000
val minMilSec = 60 * secMilSec
val hourMilSec = 60 * minMilSec
val dayMilSec = 24 * hourMilSec
val hours = (time % dayMilSec / hourMilSec).toInt()
val minutes = (time % dayMilSec % hourMilSec / minMilSec).toInt()
val seconds = (time % dayMilSec % hourMilSec % minMilSec / secMilSec).toInt()
Text(text = String.format("%02d:%02d:%02d", hours, minutes, seconds))
Composable only recompose when there is state change either from the Composable function param or by the value change of State<T> inside the Composable itself like mutableStateOf() or mutableStateListOf(). In your case, you haven't start the countDownTimer itself. Try to call countDownTimer.start() inside the DisposableEffect. Second you set the timeData with the wrong value, try to set it with millisUntilFinished
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.
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)
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)
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>
}
I have a composable which gets an integer which is a mutable state !
The integer is a timer value ! like 0 which increases on every second
What i must do inside the composable is convert to representable string like for 60 seconds I will display 1minute
so I do this
val timeString = remember {
mutableStateOf("00:00")
}
val durationString = if(duration!=0) {
var secondTime = ((duration / 1000) % 60).toString()
var minuteTime = ((((duration / 1000) / 60) % 60)).toString()
if (secondTime.length == 1) {
secondTime = "0$secondTime"
}
if (minuteTime.length == 1) {
minuteTime = "0$minuteTime"
}
"$minuteTime:$secondTime"
}else{
"00:00"
}
timeString.value = durationString
Text(
modifier = Modifier
.weight(1f)
.padding(horizontal = 16.dp),
text = timeString.value,
style = MaterialTheme.typography.h4
)
But the text does not update but since duration is a mutable state if I use it directly the text updates , so I tried LaunchedEffect but it did't work I would like to know what is the best way I can do this in compose !
Like mentioned in my comment you need to warp what should be recalculated each time inside a remember and pass the state as a key (needed so that compose knows that it has to update the String).
See example code:
#Composable
fun outer() {
val durationState = remember { mutableStateOf(600000) }
LaunchedEffect(durationState) {
// This not how you do a timer but close enough
val step = 1000
repeat(600000.div(step)) {
durationState.value = durationState.value - step
delay(step.toLong())
}
}
questionComposable(durationState)
}
#Composable
fun questionComposable(durationState: MutableState<Int>) {
val durationString = remember(durationState.value) {
val duration = durationState.value
if (duration != 0) {
var secondTime = ((duration / 1000) % 60).toString()
var minuteTime = ((((duration / 1000) / 60) % 60)).toString()
if (secondTime.length == 1) {
secondTime = "0$secondTime"
}
if (minuteTime.length == 1) {
minuteTime = "0$minuteTime"
}
"$minuteTime:$secondTime"
} else {
"00:00"
}
}
Text(
text = durationString,
style = MaterialTheme.typography.h4
)
}
I'm a Kotlin newbee. I am trying to program a countdown timer for an archery countdown clock. First the countdown timer countDownP runs for 10 sec, then runs countdown timer countDown for 60 sec. However the two timers won't run sequentially unless I nest the 2nd timer inside the onFinish() of the 1st timer.
Any suggestions would be most appreciated.
Here's the code
import ...
class MainActivity : AppCompatActivity() {
//initialize clock countdown timers for prep and shoot
internal lateinit var countDownP: CountDownTimer
internal lateinit var countDown: CountDownTimer
internal var isClockRun = false
// set-up initial clock values
internal val endTime: Long = 60000
internal val prepTime: Long = 10000
internal val warn1Time: Long = 45000
internal val totEnds: Int = 10
internal val totPEnds: Int = 3
internal var totLine: Int = 6
internal var totTurn: Int = 3
// set-up initial clock variables
internal var endNo: Int = 1
internal var endPNo: Int = 1
internal var turnNo: Int = 1
internal var isEndP = true
internal var sUntilFinished: Long = 0
//******* PROGRAM CODE STARTS ***********
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//set-up various views on screen
// set-up initial view on starting
// **START BUTTON**
startButton.setOnClickListener {
countDownP = object : CountDownTimer(prepTime, 500) {
init {
}
override fun onTick(msUntilFinished: Long) {
sUntilFinished = msUntilFinished / 1000
resultTextViewTimer.text = "${(sUntilFinished / 60)}:${(sUntilFinished % 60).toString().padStart(2, '0')}"
}
override fun onFinish() {
// when prep countdownP finished, start main countdown
isClockRun = false
isEndP = false
}
}
countDown = object : CountDownTimer(endTime, 500) {
init {
isClockRun = true
}
override fun onTick(msUntilFinished: Long) {
sUntilFinished = msUntilFinished / 1000
if (msUntilFinished <= warn1Time) {
}
resultTextViewTimer.text = "${(sUntilFinished / 60)}:${(sUntilFinished % 60).toString().padStart(2, '0')}"
}
override fun onFinish() {
isClockRun = false
}
}
if (!isClockRun) {
if (isEndP) {
isClockRun = true
countDownP.start()
} else {
isClockRun = true
countDown.start()
}
}
}
// **STOP BUTTON**
stopButton.setOnClickListener {
if (isClockRun) {
isClockRun = false
if (isEndP) countDownP.cancel()
if (!isEndP) countDown.cancel()
isEndP = true
}
}
Why don't you make a timer for 70 seconds and do the work of countDownP for 10 seconds and then do the work of countDown for 60 seconds.
In this way, I think it will be more efficient.
I want to implement timer using Kotlin coroutines, something similar to this implemented with RxJava:
Flowable.interval(0, 5, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.subscribe {
setDateTime(it)
}
It will emit LocalDateTime every new minute.
Edit: note that the API suggested in the original answer is now marked #ObsoleteCoroutineApi:
Ticker channels are not currently integrated with structured concurrency and their api will change in the future.
You can now use the Flow API to create your own ticker flow:
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)
}
}
And you can use it in a way very similar to your current code:
tickerFlow(5.seconds)
.map { LocalDateTime.now() }
.distinctUntilChanged { old, new ->
old.minute == new.minute
}
.onEach {
setDateTime(it)
}
.launchIn(viewModelScope) // or lifecycleScope or other
Note: with the code as written here, the time taken to process elements is not taken into account by tickerFlow, so the delay might not be regular (it's a delay between element processing). If you want the ticker to tick independently of the processing of each element, you may want to use a buffer or a dedicated thread (e.g. via flowOn).
Original answer
I believe it is still experimental, but you may use a TickerChannel to produce values every X millis:
val tickerChannel = ticker(delayMillis = 60_000, initialDelayMillis = 0)
repeat(10) {
tickerChannel.receive()
val currentTime = LocalDateTime.now()
println(currentTime)
}
If you need to carry on doing your work while your "subscribe" does something for each "tick", you may launch a background coroutine that will read from this channel and do the thing you want:
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()
If you want to stop from inside the loop, you can simply break out of it, and then cancel the channel:
val ticker = ticker(500, 0)
var count = 0
for (event in ticker) {
count++
if (count == 4) {
break
} else {
println(count)
}
}
ticker.cancel()
A very pragmatic approach with Kotlin Flows could be:
// 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}")
}
another possible solution as a reusable kotlin extension of CoroutineScope
fun CoroutineScope.launchPeriodicAsync(
repeatMillis: Long,
action: () -> Unit
) = this.async {
if (repeatMillis > 0) {
while (isActive) {
action()
delay(repeatMillis)
}
} else {
action()
}
}
and then usage as:
var job = CoroutineScope(Dispatchers.IO).launchPeriodicAsync(100) {
//...
}
and then to interrupt it:
job.cancel()
another note: we consider here that action is non-blocking and does not take time.
You can create a countdown timer like this
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!"
}
Here's a possible solution using 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()
}
}
Usage
val job = CoroutineScope(Dispatchers.Main).launch {
tickFlow(125L).collect {
print(it)
}
}
...
job.cancel()
Edit: Joffrey has edited his solution with a better approach.
Old :
Joffrey's solution works for me but I ran into a problem with the for loop.
I have to cancel my ticker in the for loop like this :
val ticker = ticker(500, 0)
for (event in ticker) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
But ticker.cancel() was throwing a cancellationException because the for loop kept going after this.
I had to use a while loop to check if the channel was not closed to not get this exception.
val ticker = ticker(500, 0)
while (!ticker.isClosedForReceive && ticker.iterator().hasNext()) {
if (...) {
ticker.cancel()
} else {
...
}
}
}
Timer with START, PAUSE and STOP functions.
Usage:
val timer = Timer(millisInFuture = 10_000L, runAtStart = false)
timer.start()
Timer class:
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
}
}
I took inspiration from here.
Here is Flow version of Observable.intervalRange(1, 5, 0, 1, TimeUnit.SECONDS) based on Joffrey's answer:
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)
Made a copy of Observable.intervalRange(0, 90, 0, 1, TimeUnit.SECONDS) ( will emit item in 90 sec each 1 sec ):
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))
}
}
}
Usage:
lifecycleScope.launch {
intervalRange(0, 90, 0, 1, TimeUnit.SECONDS)
.onEach {
Log.d(TAG, "intervalRange: ${90 - it}")
}
.lastOrNull()
}
Used this recently to chunk values based on a timer and max buffer size.
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)
}
}
It's not using Kotlin coroutines, but if your use case is simple enough you can always just use something like a fixedRateTimer or timer (docs here) which resolve to JVM native Timer.
I was using RxJava's interval for a relatively simple scenario and when I switched to using Timers I saw significant performance and memory improvements.
You can also run your code on the main thread on Android by using View.post() or it's mutliple variants.
The only real annoyance is you'll need to keep track of the old time's state yourself instead of relying on RxJava to do it for you.
But this will always be much faster (important if you're doing performance critical stuff like UI animations etc) and will not have the memory overhead of RxJava's Flowables.
Here's the question's code using a 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
}
}
enter image description here
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()
My solution,You can now use the Flow API to create your own ticker flow: