Search code examples
androidkotlincountdowncountdowntimerflicker

CountDownTimer flickers between seconds


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

Solution

  • 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