Flutter Countdown Timer works in Background

Navin Kumar
6 min readAug 17, 2023

Timer which works on both foreground and background using the simplest approach without any plugins.

Intro

In this blog, we will see how to run a countdown timer that works both foreground and background threads without any plugin in NavTimer flutter project.

Why this blog

As a native developer, I thought it was easy to implement a background countdown timer in Flutter for both platforms.

But here comes the problem,

ISSUES

  • The default flutter Timer class will work only in the foreground, not the background. when the app goes to the background on ios then the timer state will be stopped automatically.
  • This will give improper timer values when the app goes to the background.
  • To overcome this, we can use Isolates, again it will not work on background in some cases.
  • To run the timer on the background we need to use plugins based on background thread operations. (work manager, flutter background service, background fetch) etc.
  • But these plugins will not comply with both Android and ios platforms.
  • So again we need to do some workarounds on both native side classes.

To show a timer countdown in a flutter, why do we need to take too much-complicated kinds of stuffs?

  • That's why this blog comes, here I will show the simplest approach using the Timer class which resolves the aforementioned issues on both platforms.

Solution

  • Using Timer class for counting each second, when the app goes to background (“on pause”) Then we will mark this time as the last reference point. and the timer will be stopped.
  • When the app comes to the foreground (“on resume”), the current time will be compared with the last reference time.
  • Then the difference between two-time points will be calculated and shown in the UI layer. Then the timer will be started with the latest end time.

Sounds good! right...

I have found many posts and solutions on this approach, but no one solution has not worked perfectly. Where some cases have been missed or not handled properly.

ok Let's dive,

TimerDifferenceHandler Class

This class will handle the time difference between the current time and the ending time when the app comes from the background.

class TimerDifferenceHandler {
static late DateTime endingTime;

static final TimerDifferenceHandler _instance = TimerDifferenceHandler();

static TimerDifferenceHandler get instance => _instance;

int get remainingSeconds {
final DateTime dateTimeNow = DateTime.now();
Duration remainingTime = endingTime.difference(dateTimeNow);
// Return in seconds
LoggerUtil.getInstance.print(
"TimerDifferenceHandler -remaining second = ${remainingTime.inSeconds}");
return remainingTime.inSeconds;
}

void setEndingTime(int durationToEnd) {
final DateTime dateTimeNow = DateTime.now();
// Ending time is the current time plus the remaining duration.
endingTime = dateTimeNow.add(
Duration(
seconds: durationToEnd,
),
);
LoggerUtil.getInstance.print("TimerDifferenceHandler -setEndingTime = ${endingTime.toLocal().toString()}");
}
}
  • on timer starts, we need to set the end time value on TimerDifferenceHandler, so that the end time will be marked.
  • once the app goes to the background(on resume), that time will be set as the ending time on TimerDifferenceHandler.
  • So when the app comes to the foreground (on resume), then that time (current time) will be calculated as the difference with the last ending time.
  • This difference time (secs) will be handled on remainingSeconds.

CountDownTimer Class

This will be the base helper class that handles all timer operations.

class CountdownTimer {
int _countdownSeconds;
late Timer _timer;
final Function(int)? _onTick;
final Function()? _onFinished;
final timerHandler = TimerDifferenceHandler.instance;
bool onPausedCalled = false;

CountdownTimer({
required int seconds,
Function(int)? onTick,
Function()? onFinished,
}) : _countdownSeconds = seconds,
_onTick = onTick,
_onFinished = onFinished;

//this will start the timer
void start() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_countdownSeconds--;
if (_onTick != null) {
_onTick!(_countdownSeconds);
}

if (_countdownSeconds <= 0) {
stop();
if (_onFinished != null) {
_onFinished!();
}
}
});
}


//on pause current remaining time will be marked as end time in timerHandler class
void pause(int endTime) {
onPausedCalled = true;
stop();
timerHandler.setEndingTime(endTime); //setting end time
}

//on resume, the diff between current time and marked end time will be get from timerHandler class
void resume() {
if(!onPausedCalled){
//if on pause not called, instead resumed will called directly.. so no need to do any operations
return;
}
if (timerHandler.remainingSeconds > 0) {
_countdownSeconds = timerHandler.remainingSeconds;
start();
} else {
stop();
_onTick!(_countdownSeconds); //callback
}
onPausedCalled = false;
}

void stop() {
_timer.cancel();
_countdownSeconds = 0;
}
}

start()

  • This will start the timer countdown on decrementing the value _countdownSeconds.
  • _countdownSeconds will be the timer end limit.

pause()

  • when the app goes to the background, onPause will be called from ui layer.
  • At this time, It will set the ending time for the TimerHandler class and the timer will be stopped.
//on pause current remaining time will be marked as end time in timerHandler class
void pause(int endTime) {
onPausedCalled = true;
stop();
timerHandler.setEndingTime(endTime); //setting end time
}
  • This will mark the last timer's remaining seconds while going background.

resume()

  • on resuming (app coming into the foreground) after some time, then the current time will be compared with the last ending time on TimerHandler class.
  • This difference time (seconds) will be again set to start() function as endtime in CountDownTimer class.
_countdownSeconds = timerHandler.remainingSeconds;
  • Whenever the app goes to the background and foregrounds routinely, then the timer endpoint is set and the time difference is calculated on the current time.
  • So the timer will run seamlessly on both threads without any issues.

stop()

  • once the timer is completed, then stop() will be triggered. This function will stop the timer process.
void stop() {
_timer.cancel();
_countdownSeconds = 0;
}

onPausecalled boolean

  • In ios, onresume can be called without onpause being called, so in this case, the timer will start multiple times and give wrong timer values.
  • So This boolean is handled to check if the app goes to the background without calling the pause() function. (This will happen on an ios device).
  • So only if pause() is called, then only the resume() function will handle timer diff operations. else returned.
void resume() {
if(!onPausedCalled){
//if on pause not called, instead resumed will called directly.. so no need to do any operations
return;
}
if (timerHandler.remainingSeconds > 0) {
_countdownSeconds = timerHandler.remainingSeconds;
start();
} else {
stop();
_onTick!(_countdownSeconds); //callback
}
onPausedCalled = false;
}

UI Layer

  • calling timer functionality on ui layer.
 int countdownSeconds = 180; //total timer limit in seconds
late CountdownTimer countdownTimer;
bool isTimerRunning = false;

void initTimerOperation()
{

//timer callbacks
countdownTimer = CountdownTimer(
seconds: countdownSeconds,
onTick: (seconds) {
isTimerRunning = true;
countdownSeconds = seconds; //this will return the timer values
},
onFinished: () {
isTimerRunning = false;
countdownTimer.stop();
// Handle countdown finished
},
);

//native app life cycle
SystemChannels.lifecycle.setMessageHandler((msg) {

// On AppLifecycleState: paused
if (msg == AppLifecycleState.paused.toString()) {
if (isTimerRunning) {
countdownTimer.pause(countdownSeconds); //setting end time on pause
}
}


// On AppLifecycleState: resumed
if (msg == AppLifecycleState.resumed.toString()) {
if (isTimerRunning) {
countdownTimer.resume();
}
}
return Future(() => null);
});


//starting timer
isTimerRunning = true;
countdownTimer?.start();

}

Conclusion

Wow great 🎉, Hope this blog is helpful to implement flutter timer functionality in an efficient manner.

Check out the full project: NavTimer

This article helped you? Long press on the 👏 button as long as you can.

I got something wrong? Mention it in the comments. I would love to improve.

I am NavinKumar, Lead Application developer @IBM. You can find me on Linkedin or stalk me on GitHub or Follow me on Medium

--

--