CountDownTimer in jetpack compose - android

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

Related

How to create a countdown with flow coroutines

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

How to recompose every minute in Jetpack Compose?

I would like to show a timer counting down in my composable, but I am not sure how to achieve this.
I was thinking to set a delay/timeout for a minute and trigger a recompose that way, but I am not sure if that's the right way to think about it.
#Composable
fun Countdown(completedAt: Date) {
val minutesLeft = ceil((completedAt.time - Date().time) / 60_000.0).toInt()
Handler(Looper.getMainLooper()).postDelayed({
// TODO: Recompose
}, 60_000)
Text(text = "$minutesLeft minutes until completed")
}
My goal is for the text to update every minute with the new time. How can I do this?
Store the amount of minutes as state.
Also make sure to clean up the postDelayed callback inside a DisposableEffect to prevent conflicting delays and memory leaks.
I have moved this logic to a minutesLeft composable function so it can be reused.
#Composable
fun minutesLeft(until: Date): Int {
var value by remember { mutableStateOf(getMinutesLeft(until)) }
DisposableEffect(Unit) {
val handler = Handler(Looper.getMainLooper())
val runnable = {
value = getMinutesLeft(until)
}
handler.postDelayed(runnable, 60_000)
onDispose {
handler.removeCallbacks(runnable)
}
}
return value
}
private fun getMinutesLeft(until: Date): Int {
return ceil((until.time - Date().time) / 60_000.0).toInt()
}
Usage
#Composable
fun Countdown(completedAt: Date) {
val minutes = minutesLeft(until = completedAt)
Text(text = "$minutes minutes until completed")
}
You can use a ViewModel with the CountDownTimer class.
Something like:
val countTimeViewModel : CountTimeViewModel = viewModel()
val minutes = countTimeViewModel.minutes.observeAsState(60)
Button( onClick={
countTimeViewModel.onStartClicked(60000*60) }
){
Text("Start")
}
Text(""+minutes.value)
with:
class CountTimeViewModel : ViewModel() {
private var timer: CountDownTimer? = null
private val _minutes = MutableLiveData(totalTime)
val minutes: LiveData<Int> get() = _minutes
private var totalTime : Long = 0L
fun startCountDown() {
timer = object : CountDownTimer(totalTime, 60000) {
override fun onTick(millisecs: Long) {
// Minutes
val minutes = (millisecs / MSECS_IN_SEC / SECS_IN_MINUTES % SECS_IN_MINUTES).toInt()
_minutes.postValue(minutes)
}
override fun onFinish() {
//...countdown completed
}
}
}
fun onStartClicked(totalTime : Long) {
this.totalTime = totalTime
startCountDown()
timer?.start()
}
override fun onCleared() {
super.onCleared()
timer?.cancel()
}
companion object {
const val SECS_IN_MINUTES = 60
const val MSECS_IN_SEC = 1000
}
}
#Composable
fun minutesLeft(until: Date): Int {
var timeout by remember { mutableStateOf(getMinutesLeft(until)) }
Text(text = "$timeout minutes until completed")
LaunchedEffect(timeout) {
if (timeout > 0) {
delay(1000 * 60)
timeout -= 1
}
}
}
private fun getMinutesLeft(until: Date): Int {
return ceil((until.time - Date().time) / 60_000.0).toInt()
}
these codes may give you an idea to achieve this goal:
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))

State depends on another state , Android Compose

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

re-Printing Chronometer value gives Integer value instead of String

I am trying to pull a String value equal to the moment the chronometer stops as in "01:21" but the elapsed time gives an integer value, as in "11322".
val chronometer = findViewById<Chronometer>(R.id.chronometer)
chronometer.base = SystemClock.elapsedRealtime()
chronometer.format = "%s"
chronometer.start()
button.setOnClickListener {
val elapsedTime = SystemClock.elapsedRealtime() - chronometer.base
header.text = elapsedtime.toString()
Toast.makeText(this,"$elapsedTime",Toast.LENGTH_SHORT).show()
}
To be precise you get a long value and not an integer because both SystemClock.elapsedRealtime() and chronometer.base return a long value.
I have to disappoint you, currently there is no way to directly get the shown time of a chronometer, but of course you can convert the milliseconds you got to minutes and seconds, so you can show it again.
Here's my example of how it could work:
private fun calculateTime(chronometerMillis : Long) {
val minutes = TimeUnit.MILLISECONDS.toMinutes(chronometerMillis)
val seconds = TimeUnit.MILLISECONDS.toSeconds(chronometerMillis) - (minutes * 60)
println("Recreated time: $minutes:$seconds")
}
If you now call this method with the value 81000, which is 1 Minute and 21 Seconds (just like your chronometer), it prints Recreated time: 1:21.
To use it in your project just return a String:
private fun calculateTime(chronometerMillis : Long) : String {
val minutes = TimeUnit.MILLISECONDS.toMinutes(chronometerMillis)
val seconds = TimeUnit.MILLISECONDS.toSeconds(chronometerMillis) - (minutes * 60)
return "$minutes:$seconds"
}
private fun formattedChronometer(chronometer: Chronometer): String {
val elapsedTime = SystemClock.elapsedRealtime() - chronometer.base
val time = elapsedTime / 1000
val minutes = time / 60
val seconds = time % 60
return String.format("%02d:%02d",minutes,seconds)
}
Managed to solve it using this function after looking into the documentation

CountdownTimer is not working as intended (wrong time)

I'm using CountdownTimer in my app to display remaining time until specific Date. But Date is only 2 hours from current time, but if I convert millisUntilFinished to hours, it says 9 hours. Date is in UTC format.
remainingTimer = object : CountDownTimer(dateTime.time, 1000) {
override fun onTick(millisUntilFinished: Long) {
remTime = millisUntilFinished
notifyChanged(PAYLOAD_UPDATE_REM_TIME)
}
override fun onFinish() {
swapTimers()
}
}.start()
val hours = ((remTime / (1000 * 60 * 60)).rem(24))
Here you have to specify timer time for how long (2 hour from now = 2*60*60*1000 millis) you want to run
if you convert date in millis its not gonna work the way you want as it return date in millis,
remainingTimer = object : CountDownTimer(2*60*60*1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
remTime = millisUntilFinished
notifyChanged(PAYLOAD_UPDATE_REM_TIME)
}
override fun onFinish() {
swapTimers()
}
}.start()
val hours = ((remTime / (1000 * 60 * 60)).rem(24))

Categories

Resources