Create A Countdown Timer That Runs Even If The App Is Closed

Alexander Portillo
Dev Genius
Published in
5 min readAug 13, 2022

--

Video Demo

I will be demonstrating how to create a count down timer that works if you exit the app and come back after a period of time. You will be able to start, stop and reset the timer. We will be using shared preferences to make this work. Scroll down to the end to see a video demo of what the app does. A link to this project’s GitHub repository is also at the end. Let’s get started :)

  1. Create an empty project in Android Studio and enable view binding.
  2. Add a textview and 2 buttons on your activity_main.xml file.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/timerTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="40sp"
android:text="05:00"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<Button
android:id="@+id/startOrStopBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/resetBtn"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/timerTV" />

<Button
android:id="@+id/resetBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Reset"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/startOrStopBtn"
app:layout_constraintTop_toBottomOf="@id/timerTV" />

</androidx.constraintlayout.widget.ConstraintLayout>

Run the app and it should look something like this.

MainActivity.kt

From this point forward, everything will be done on MainActivity.kt. I will try my best to explain what each function does and at the very end, I will paste all the code in MainActivity.kt.

First off, create these variables in MainActivity.

private lateinit var binding: ActivityMainBinding
private var timeLeftInMillis = 0L
private var countDownTimer: CountDownTimer? = null
private var timerIsRunning = false
private var remainingTimeInMillis = 300000L
private var endTime = 0L

Inside of onCreate(), add onClickListeners to both the start and reset buttons.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.startOrStopBtn.setOnClickListener {
startOrStopTimer()
}

binding.resetBtn.setOnClickListener {
stopOrResetTimer()
}

}

startOrStopTimer()

private fun startOrStopTimer() {
if (!timerIsRunning) {
countDownTimer?.cancel()
binding.startOrStopBtn.text = "Stop"
countDownTimer = object : CountDownTimer(remainingTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainingTimeInMillis = millisUntilFinished
timeLeftInMillis = millisUntilFinished
timerIsRunning = true

binding.timerTV.text = millisUntilFinished.convertToTimeFormat()
}

override fun onFinish() {
timerIsRunning = false
remainingTimeInMillis = 0L
timeLeftInMillis = 0L
binding.startOrStopBtn.text = "Start"
}
}.start()
} else {
countDownTimer?.cancel()
timerIsRunning = false
binding.startOrStopBtn.text = "Start"
}
}

In this function we are checking if the timer is not running. If it is not, we create a CountDownTimer() with a duration of “remainingTimeInMillis”. The value of remainingTimeInMillis will either be 5 minutes if the user clicked on the reset button or the value will be the time where the timer was previously stopped at. The onTick() function is called every second because we set the countDownInterval to be 1000 milliseconds. So every second we are updating the remainingTimeInMillis, timeLeftInMillis and timerIsRunning. I will talk about what the converToTimeLeftFormat() fuction does in a bit. The onFinish() function is called when the time is up.

stopOrResetTimer()

private fun stopOrResetTimer() {
countDownTimer?.cancel()
timerIsRunning = false
timeLeftInMillis = 300000
remainingTimeInMillis = 300000
binding.timerTV.text = remainingTimeInMillis.convertToTimeFormat()
binding.startOrStopBtn.text = "Start"
}

This is function is pretty simple. We are basically setting the time back to 300000 milliseconds, which is 5 minutes. Then setting the timerIsRunning to false, updating the textview so that it shows “05:00” and changing the button text to “START” instead of “STOP”.

Long.convertToTimeFormat()

private fun Long.convertToTimeFormat(): String {
val minutes = (this / 1000).toInt() / 60
val seconds = (this / 1000).toInt() % 60

return java.lang.String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
}

This function is an extension function and it just converts milliseconds to this type of format: “04:10”, where 4 is the minutes and 10 is seconds.

onStart()

override fun onStart() {
super.onStart()
val prefs = getSharedPreferences("COUNT_DOWN_TIMER", MODE_PRIVATE)
timeLeftInMillis = prefs.getLong("millisecondsLeft", 0L)
timerIsRunning = prefs.getBoolean("isTimerRunning", false)
endTime = prefs.getLong("endTimeInMillis", 0L)

if (endTime != 0L && timerIsRunning) {
val timeSpentInBackground: Long = (System.currentTimeMillis() - endTime)
remainingTimeInMillis = timeLeftInMillis - timeSpentInBackground
} else {
remainingTimeInMillis = timeLeftInMillis
}
binding.timerTV.text = remainingTimeInMillis.convertToTimeFormat()

if (remainingTimeInMillis != 0L && timerIsRunning) {

binding.startOrStopBtn.text = "Stop"
countDownTimer = object : CountDownTimer (remainingTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainingTimeInMillis = millisUntilFinished
timeLeftInMillis = millisUntilFinished
timerIsRunning = true

binding.timerTV.text = millisUntilFinished.convertToTimeFormat()
}

override fun onFinish() {
timerIsRunning = false
remainingTimeInMillis = 0L
timeLeftInMillis = 0L
binding.startOrStopBtn.text = "Start"
}
}.start()
}
}

This function is basically getting the variables from shared preferences and checking to see at what time the user clicked out of the app and retrieves a boolean if the timer was running. If the timer was running when the user clicked out of the app, it calculates the time that was spent away from the app. It then starts a timer with the remaining time.

onStop()

override fun onStop() {
super.onStop()
countDownTimer?.cancel()

val prefs = getSharedPreferences("COUNT_DOWN_TIMER", MODE_PRIVATE)
val editor = prefs.edit()
editor.putLong("millisecondsLeft", timeLeftInMillis)
editor.putBoolean("isTimerRunning", timerIsRunning)
editor.putLong("endTimeInMillis", System.currentTimeMillis())
editor.commit()
}

Saves millisecondsLeft on to saved preferences. This is the amount of time left in the timer in milliseconds. It also saves “isTimerRunning”, a boolean that checks if the timer was running when the user left the app. And lastly “endTimeInMillis”, which is the exact time when the user left the app.

The final code in MainActivity() should look like this.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private var timeLeftInMillis = 0L
private var countDownTimer: CountDownTimer? = null
private var timerIsRunning = false
private var remainingTimeInMillis = 300000L
private var endTime = 0L

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.startOrStopBtn.setOnClickListener {
startOrStopTimer()
}

binding.resetBtn.setOnClickListener {
stopOrResetTimer()
}

}

private fun startOrStopTimer() {
if (!timerIsRunning) {
countDownTimer?.cancel()
binding.startOrStopBtn.text = "Stop"
countDownTimer = object : CountDownTimer(remainingTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainingTimeInMillis = millisUntilFinished
timeLeftInMillis = millisUntilFinished
timerIsRunning = true

binding.timerTV.text = millisUntilFinished.convertToTimeFormat()
}

override fun onFinish() {
timerIsRunning = false
remainingTimeInMillis = 0L
timeLeftInMillis = 0L
binding.startOrStopBtn.text = "Start"
}
}.start()
} else {
countDownTimer?.cancel()
timerIsRunning = false
binding.startOrStopBtn.text = "Start"
}
}

private fun stopOrResetTimer() {
countDownTimer?.cancel()
timerIsRunning = false
timeLeftInMillis = 300000
remainingTimeInMillis = 300000
binding.timerTV.text = remainingTimeInMillis.convertToTimeFormat()
binding.startOrStopBtn.text = "Start"
}

private fun Long.convertToTimeFormat(): String {
val minutes = (this / 1000).toInt() / 60
val seconds = (this / 1000).toInt() % 60

return java.lang.String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
}

override fun onStart() {
super.onStart()
val prefs = getSharedPreferences("COUNT_DOWN_TIMER", MODE_PRIVATE)
timeLeftInMillis = prefs.getLong("millisecondsLeft", 0L)
timerIsRunning = prefs.getBoolean("isTimerRunning", false)
endTime = prefs.getLong("endTimeInMillis", 0L)

if (endTime != 0L && timerIsRunning) {
val timeSpentInBackground: Long = (System.currentTimeMillis() - endTime)
remainingTimeInMillis = timeLeftInMillis - timeSpentInBackground
} else {
remainingTimeInMillis = timeLeftInMillis
}
binding.timerTV.text = remainingTimeInMillis.convertToTimeFormat()

if (remainingTimeInMillis != 0L && timerIsRunning) {

binding.startOrStopBtn.text = "Stop"
countDownTimer = object : CountDownTimer (remainingTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
remainingTimeInMillis = millisUntilFinished
timeLeftInMillis = millisUntilFinished
timerIsRunning = true

binding.timerTV.text = millisUntilFinished.convertToTimeFormat()
}

override fun onFinish() {
timerIsRunning = false
remainingTimeInMillis = 0L
timeLeftInMillis = 0L
binding.startOrStopBtn.text = "Start"
}
}.start()
}
}

override fun onStop() {
super.onStop()
countDownTimer?.cancel()

val prefs = getSharedPreferences("COUNT_DOWN_TIMER", MODE_PRIVATE)
val editor = prefs.edit()
editor.putLong("millisecondsLeft", timeLeftInMillis)
editor.putBoolean("isTimerRunning", timerIsRunning)
editor.putLong("endTimeInMillis", System.currentTimeMillis())
editor.commit()
}
}

GitHub Repository

https://github.com/alexportillo519/CountDownTimer

Hope this helps. Let me know if you have any questions. Happy coding!

--

--