I am trying to develop a timer app in Flutter that includes multiple countdown timers for different tasks. However, I'm facing an issue where if I start a timer and then press the back button, the timer stops. I want the timer to continue running until it is either paused or it reaches the end, even if the user navigates away from the app or the app is closed. I am also using SQLite to store all the timers. How can I achieve this functionality in Flutter?
Ah, background tasks! Dart (the language Flutter uses) is single-threaded.
What does single-threaded
mean?
Single-threaded languages such as Dart have something called an event loop. That means that Dart runs code line by line (unless you use Futures but that won't help you in this case). It registers events like button taps and waits for users to press them, etc.
I recommend this article and video on single-threaded stuff:
https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
https://www.youtube.com/watch?v=vl_AaCgudcY&feature=emb_logo
Anyways, the way to combat this (as mentioned in the article and video above) is Isolates. When you create an Isolate in Dart, it spins up another thread to do heavy tasks or just something while the app may or may not be in focus. That way, the main thread can load things like UI while in another thread, it takes care of the other stuff you put in it, therefore, increasing the performance of your app.
How does it relate to your question?
You can use Isolates to execute tasks in the background of your app (open or not).
Essentially it uses Timer.periodic
inside an isolate to execute tasks which is perfect for your scenario.
Isolates won't solve the problem. even isolates gets paused by the OS when the app is in the background. I have tried it (in Android10 timer runs for about 100 seconds before it gets paused by the system).
for your problem Android_Alarm_Manager package seems to be the solution. but it only supports android. no ios support yet
I had a similar problem. The isolate won't solve it since the isolates life cycle is also linked to the main isolate(ui thread). I sort of created a workaround,i.e I used the dateTime property,so if the user chooses 30 minutes,the final Date time becomes Date time.now(Duration (minutes:30));. And a timer periodic function which runs every minute and finds the difference between the final date time and the datetime.now() so even if the os kills ur app while in the background application tray when u reopen the app,the time automatically updates to the change. I also used flutter notification package to alert the user when the timer value becomes zero
I worked on a flutter project with the exact scenario that you have described.
Let me first tell you, Isolates, as proposed above, won't work for you here. Isolates seem to get paused when your app is not in the foreground. There is no single reason why/when the OS kills background isolates - Android & iOS implement ML driven process management programs that kill threads to optimise battery life, usage etc.
I took a rather rudimentary approach using shared_preferences. (I needed to have my timer continue running even if the app is completely shut from the app switcher as well i.e. not in the background or foreground). Here is what I did:
When the app is in the foreground and the timer is started, I listen on Flutter's lifecycle hooks for state change. As soon as the app is removed from the foreground (killed or moved to b/g), I save the timer value in shared_preferences. When the app is in the foreground again, I check for active timers from my shared_prefences, calculate the difference between the saved timer and DateTime.now()
and resume the timer from the adjusted offset.
Faced the same problem and found a solution. I used Hook but I hope you understand the principle
parent class:
useEffect(() {
if (state == AppLifecycleState.resumed) {
lostTimeNotifier.value = handleExpireDate(context);
}
return null;
});
then we listen to it:
return HookBuilder(builder: (context) {
final lostTime = useValueListenable(lostTimeNotifier);
return CountDownExpireCartWidget(
lostSeconds: lostTime,
);
Now look at child widget:
class CountDownExpireCartWidget extends Hook {...
@override
Widget build(BuildContext context) {
timer? timer;
final isFinished = useValueNotifier(false);
final timeNotifier = useValueNotifier(lostSeconds);
void startTimer() {
const oneSec = Duration(seconds: 1);
timer = Timer.periodic(
onesec,
(timer) {
if (timeNotifier.value < 1) {
callBack.call();
isFinished.value = true;
timer.cancel();
} else {
timeNotifier.value--;
}
},
);
}
every time lostTimeNotifier its value is updated it will be called in this method, this is the key, the internal notifier will already be updated and give the current value
useEffect(() {
timeNotifier.value = lostSeconds;
if (!isFinished.value) {
startTimer();
return() {
timer?.cancel();
};
}
return null;
});
Now we are listening child notifier:
HookBuilder(builder: (_) {
final timer = useValueListenable(timeNotifier);
return Text('Lost seconds: $timer');
Hope it helps someone)
Try this approach,
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()}");
}
}
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;
}
}
I hope this will be helpful. One way I was able to implement a stopwatch to run in the background that would easily be implemented into my logic was to add the difference in time between resumed and paused instances in the app using the didChangeAppLifeCycleState function to the stopwatch. Below is some pretty simple logic that I added to my code.
Basically, if the phone is asleep or the app is in the paused state, what you do is track the time once the app is paused to the time once the app is resumed. This isn't a complete solution for timers specifically, but it does work for stopwatches. There can be some workaround for keeping the persistence of timers, but this doesn't solve the issue of say playing a sound when the timer goes off. You'll probably need to use some dependency that deals with system notifications for that.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (timerGo) {
pausedEndTime = DateTime.now();
minLeft += pausedEndTime.difference(pausedStartTime).inMinutes;
secLeft += pausedEndTime.difference(pausedStartTime).inSeconds -
60 * pausedEndTime.difference(pausedStartTime).inMinutes;
startTimer();
}
print('resumed');
}
if (state == AppLifecycleState.inactive) {
print('inactive');
}
if (state == AppLifecycleState.detached) {
print('detached');
}
if (state == AppLifecycleState.paused) {
print('paused');
if (timerGo) {
_timer.cancel();
pausedStartTime = DateTime.now();
}
}
}
© 2022 - 2024 — McMap. All rights reserved.