I have a CountDownTimer that runs fine most times. However, it at times begins to flicker in the TextView displaying the countdown. I have it displayed like so:
HH:MM:SS
As it's counting down the last second will jump from for example 9 to 7 to 8 in one second. And then the next second it will quickly flicker from 8 to 6 to 7.
I have tried passing the variable millisUntilFinished directly to the method updating the textView, but the issue persists. Note that I'm saving the countdown and continuing it during onStop and onStart methods.
private fun startVisibleCountdown() {
visibleCountdownRunning = true
object : CountDownTimer(timeLeftInMillisecondsVisibleCounter, 1000) {
override fun onTick(millisUntilFinished: Long) {
timeLeftInMillisecondsVisibleCounter = millisUntilFinished
updateCountDownTextVisible()
}
override fun onFinish() {
//not relevant here
}
}.start()
}
fun updateCountDownTextVisible() {
var seconds = (timeLeftInMillisecondsVisibleCounter / 1000).toInt()
val hours = seconds / (60 * 60)
val tempMint = seconds - hours * 60 * 60
val minutes = tempMint / 60
seconds = tempMint - (minutes * 60);
textViewTimer.text = (String.format("%02d", hours)
+ ":" + String.format("%02d", minutes)
+ ":" + String.format("%02d", seconds))
}
Saving and returning to countdown when app is closed:
override fun onStop() {
super.onStop()
if (visibleCountdownRunning) {
timeLeftInMillisecondsVisibleCounter += System.currentTimeMillis()
}
val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return
with (sharedPref.edit()) {
putLong(timeLeftVisibleCounterKey, timeLeftInMillisecondsVisibleCounter)
putBoolean(visibleCountdownRunningKey, visibleCountdownRunning)
apply()
}
}
override fun onStart() {
super.onStart()
val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return
timeLeftInMillisecondsVisibleCounter = sharedPref.getLong(timeLeftVisibleCounterKey, 43200000)
visibleCountdownRunning = sharedPref.getBoolean(visibleCountdownRunningKey, false)
if (visibleCountdownRunning) {
timeLeftInMillisecondsVisibleCounter -= System.currentTimeMillis()
if (timeLeftInMillisecondsVisibleCounter > 0) {
startVisibleCountdown()
} else {
timeLeftInMillisecondsVisibleCounter = 43200000
visibleCountdownRunning = false
}
}
}
Every time onStart
fires (e.g. by going to the home screen then back into the app) if the current timer still has time left, you start another one through that startVisibleCountdown()
call. You haven't shown you're stopping the old one anywhere - your code isn't even keeping a reference to it, it's just started anonymously, and runs until it completes (or the app is killed)
If you have multiple timers running, they all set timeLeft
according to the value passed into their onTick
, and then they call the function that displays that. Since their updates are all getting posted to the message queue, it's possible there's a slight timing discrepancy between them (e.g. one has millisUntilFinished = 6000
and one has 5999
) and you're getting them out of order.
That would explain why it's changing at all (you have multiple timers setting the text on the TextView
when there should only be one) and why it's going backwards (no hard guarantees about which message is at the front of the queue or even when it arrives exactly)
So you need to make sure you're only ever running one instance of your timer - there are a few ways to handle that, this is probably the safest approach (not thread-safe but that's not an issue with what you're doing here):
private var myTimer: CountdownTimer? = null
...
fun startVisibleCountdown() {
// enforce a single current timer instance
if (myTimer != null) return
myTimer = object : CountDownTimer(timeLeftInMillisecondsVisibleCounter, 1000) {
...
override fun onFinish() {
// if a timer finishes (i.e. it isn't cancelled) clear it so another can be started
myTimer = null
}
}
}
...
override fun onStop() {
...
// probably better in a function called stopVisibleCountdown() for symmetry and centralising logic
myTimer?.run { cancel() }
myTimer = null
}
That way, there's only one place you're actually creating and starting a timer, and that code ensures only one instance is running at a time. That instance is only cleared when the timer successfully finishes, or when it's explicitly stopped. By centralising it like this (and putting all your defensive coding in one place) you can call the start function from multiple places (onStart
, some button's onClick
) and it will all be handled safely