I made a countdown timer and I want it to keep running even after orientation change. But say when I start the timer and then change orientation and then pause it, the timer seems to be running in the background.
I checked if it's running in background or not by displaying a toast message in onFinish()
function and it seems to be running in the background.
import android.os.CountDownTimer;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
private static final long START_TIME_IN_MILLIS = 10000;
private TextView mTextViewCountDown;
private Button mButtonStartPause;
private Button mButtonReset;
private CountDownTimer mCountDownTimer;
private boolean mTimerRunning;
private long mTimeLeftInMillis = START_TIME_IN_MILLIS;
private long mEndTime;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextViewCountDown = findViewById(R.id.text_view_countdown);
mButtonStartPause = findViewById(R.id.button_start_pause);
mButtonReset = findViewById(R.id.button_reset);
mButtonStartPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTimerRunning) {
pauseTimer();
} else {
startTimer();
}
}
});
mButtonReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resetTimer();
}
});
updateCountDownText();
}
private void startTimer() {
mEndTime = System.currentTimeMillis() + mTimeLeftInMillis;
mCountDownTimer = new CountDownTimer(mTimeLeftInMillis, 1000) {
@Override
public void onTick(long millisUntilFinished) {
mTimeLeftInMillis = millisUntilFinished;
updateCountDownText();
}
@Override
public void onFinish() {
mTimerRunning = false;
Toast.makeText(MainActivity.this, "TIMER RUNNING IN BACKGROUND..!!! ", Toast.LENGTH_SHORT).show();
updateButtons();
}
}.start();
mTimerRunning = true;
updateButtons();
}
private void pauseTimer() {
mCountDownTimer.cancel();
mTimerRunning = false;
updateButtons();
}
private void resetTimer() {
mTimeLeftInMillis = START_TIME_IN_MILLIS;
updateCountDownText();
updateButtons();
}
private void updateCountDownText() {
int minutes = (int) (mTimeLeftInMillis / 1000) / 60;
int seconds = (int) (mTimeLeftInMillis / 1000) % 60;
String timeLeftFormatted = String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
mTextViewCountDown.setText(timeLeftFormatted);
}
private void updateButtons() {
if (mTimerRunning) {
mButtonReset.setVisibility(View.INVISIBLE);
mButtonStartPause.setText("Pause");
} else {
mButtonStartPause.setText("Start");
if (mTimeLeftInMillis < 1000) {
mButtonStartPause.setVisibility(View.INVISIBLE);
} else {
mButtonStartPause.setVisibility(View.VISIBLE);
}
if (mTimeLeftInMillis < START_TIME_IN_MILLIS) {
mButtonReset.setVisibility(View.VISIBLE);
} else {
mButtonReset.setVisibility(View.INVISIBLE);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong("millisLeft", mTimeLeftInMillis);
outState.putBoolean("timerRunning", mTimerRunning);
outState.putLong("endTime", mEndTime);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mTimeLeftInMillis = savedInstanceState.getLong("millisLeft");
mTimerRunning = savedInstanceState.getBoolean("timerRunning");
updateCountDownText();
updateButtons();
if (mTimerRunning) {
mEndTime = savedInstanceState.getLong(enter code here"endTime");
mTimeLeftInMillis = mEndTime - System.currentTimeMillis();
startTimer();
}
}
}
Is there a way to solve this?
You could move the countdown logic and data to a ViewModel
The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.
It's an architecture component used in the Recommended app architecture.
The MainViewModel code could be:
package com.example.countdown;
import androidx.lifecycle.ViewModel;
public class MainViewModel extends ViewModel {
private static final long START_TIME_IN_MILLIS = 10000;
private boolean mTimerRunning;
private long mTimeLeftInMillis = START_TIME_IN_MILLIS;
private long mEndTime;
boolean getTimerRunning() {
return mTimerRunning;
}
long getTimeLeftInMillis() {
return mTimeLeftInMillis;
}
long getEndTime() {
return mEndTime;
}
void startTimer() {
mEndTime = System.currentTimeMillis() + mTimeLeftInMillis;
mTimerRunning = true;
}
void restoreTimer(boolean timerRunning, long timeLeftInMillis, long endTime) {
this.mTimerRunning = timerRunning;
this.mTimeLeftInMillis = timeLeftInMillis;
this.mEndTime = endTime;
}
void finish() {
mTimerRunning = false;
}
void restart() {
mTimeLeftInMillis = START_TIME_IN_MILLIS;
}
void updateCounter(long millisUntilFinished) {
mTimeLeftInMillis = millisUntilFinished;
}
boolean remainingTime() {
return mTimeLeftInMillis < START_TIME_IN_MILLIS;
}
}
And the MainActivity:
package com.example.countdown;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
protected MainViewModel viewModel;
private TextView mTextViewCountDown;
private Button mButtonStartPause;
private Button mButtonReset;
private CountDownTimer mCountDownTimer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
mTextViewCountDown = findViewById(R.id.text_view_countdown);
mButtonStartPause = findViewById(R.id.button_start_pause);
mButtonReset = findViewById(R.id.button_reset);
mButtonStartPause.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (viewModel.getTimerRunning()) {
pauseTimer();
} else {
startTimer();
}
}
});
mButtonReset.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resetTimer();
}
});
updateCountDownText();
}
@Override
protected void onPause() {
super.onPause();
if (mCountDownTimer != null) {
mCountDownTimer.cancel();
}
}
@Override
protected void onResume() {
super.onResume();
if (viewModel.getTimerRunning()) {
startTimer();
}
}
@Override
protected void onDestroy() {
viewModel.finish();
super.onDestroy();
}
private void startTimer() {
viewModel.startTimer();
mCountDownTimer = new CountDownTimer(viewModel.getTimeLeftInMillis(), 1000) {
@Override
public void onTick(long millisUntilFinished) {
viewModel.updateCounter(millisUntilFinished);
updateCountDownText();
}
@Override
public void onFinish() {
viewModel.finish();
Toast.makeText(MainActivity.this, "Timer finished", Toast.LENGTH_SHORT).show();
updateButtons();
}
}.start();
updateButtons();
}
private void pauseTimer() {
if (mCountDownTimer != null) {
mCountDownTimer.cancel();
}
viewModel.finish();
updateButtons();
}
private void resetTimer() {
viewModel.restart();
updateCountDownText();
updateButtons();
}
private void updateCountDownText() {
int minutes = (int) (viewModel.getTimeLeftInMillis() / 1000) / 60;
int seconds = (int) (viewModel.getTimeLeftInMillis() / 1000) % 60;
String timeLeftFormatted = String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds);
mTextViewCountDown.setText(timeLeftFormatted);
}
private void updateButtons() {
if (viewModel.getTimerRunning()) {
mButtonReset.setVisibility(View.INVISIBLE);
mButtonStartPause.setText("Pause");
} else {
mButtonStartPause.setText("Start");
if (viewModel.getTimeLeftInMillis() < 1000) {
mButtonStartPause.setVisibility(View.INVISIBLE);
} else {
mButtonStartPause.setVisibility(View.VISIBLE);
}
if (viewModel.remainingTime()) {
mButtonReset.setVisibility(View.VISIBLE);
} else {
mButtonReset.setVisibility(View.INVISIBLE);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong("millisLeft", viewModel.getTimeLeftInMillis());
outState.putBoolean("timerRunning", viewModel.getTimerRunning());
outState.putLong("endTime", viewModel.getEndTime());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
updateCountDownText();
updateButtons();
long timeLeftInMillis = savedInstanceState.getLong("millisLeft");
if (viewModel.getTimerRunning()) {
timeLeftInMillis = viewModel.getEndTime() - System.currentTimeMillis();
startTimer();
}
viewModel.restoreTimer(savedInstanceState.getBoolean("timerRunning"),
timeLeftInMillis,
savedInstanceState.getLong("endTime"));
}
}
Notice how it gets the ViewModel using ViewModelProvider(), once the activity is recreated it will receive the same ViewModel instance, which is scoped to this activity.
Also, don't forget to destroy the timer in onDestroy(), when this activity is finished and not recreated.