4

I am currently working on a timer app in flutter and use Dart Isolate for the timer to work in background.

It really works well in ios Simulator(iPhone 12 Pro) and android devices.

In ios real devices, when the app is run in background, the timer completely pause, and resume when the app is brought back from background.

Is there a way to make the timer to run in background in iOS devices?

TimerTask class:

import 'dart:async';
import 'dart:isolate';

typedef IsolateEnvironmentEntryPoint(
    SendPort sendPort, ReceivePort receivePort);

class TimerTask {
  Isolate isolate;
  ReceivePort receivePort;
  SendPort sendPort;

  TimerTask._(this.isolate, this.receivePort, this.sendPort);

  static Future<TimerTask> spawn(
      IsolateEnvironmentEntryPoint entryPoint) async {
    var completer = Completer<TimerTask>();
    var isolateReceivePort = ReceivePort();
    var envReceivePort = ReceivePort();

    Isolate isolate;

    isolateReceivePort.listen((msg) {
      if (msg is SendPort) {
        completer.complete(TimerTask._(isolate, envReceivePort, msg));
      } else {
        envReceivePort.sendPort.send(msg);
      }
    });

    var args = [entryPoint, isolateReceivePort.sendPort];
    isolate = await Isolate.spawn(backgroundTimerTask, args);
    return completer.future;
  }

  static void backgroundTimerTask(List args) {
    IsolateEnvironmentEntryPoint entryPoint = args[0];
    SendPort sendPort = args[1];

    var receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    entryPoint(sendPort, receivePort);
  }

  void stopTimerBgTask() {
    if (isolate != null) {
      isolate?.kill(priority: Isolate.immediate);

      isolate = null;

      print('Killed');
    }
  }
}

In main thread,

 timer = await TimerTask.spawn(runBackgroundTimerTask);
    
          timer.receivePort.asBroadcastStream().listen((msg) {
            if (msg[0] == 'TimerRunInProgress') {
              // setState to UI 
            } else if (msg[0] == 'TimerPaused') {
              // setState to UI 
            } else if (msg[0] == 'ResumeTimer') {
              // setState to UI 
            } else if (msg[0] == 'ResetTimer') {
              // setState to UI 
            }
          });
        }

And the top-level isolate method runBackgroundTimerTask() is like this:

runBackgroundTimerTask(SendPort sendPort, ReceivePort receivePort) {
    receivePort.listen((msg) {
      
      if (msg[0] == 'TimerStarted') {
        _tickerSubscription?.cancel();
        _tickerSubscription = _ticker.tick(ticks: msg[1]).listen((timer) {              
          sendPort.send(['TimerRunInProgress', timer]);
        });
      } else if (msg[0] == 'PauseTimer') {
        _tickerSubscription?.pause();
        sendPort.send(['TimerPaused']);
      } else if (msg[0] == 'ResumeTimer') {
        _tickerSubscription?.resume();
        sendPort.send(['ResumeTimer']);
      } else if (msg[0] == 'ResetTimer') {
        _tickerSubscription?.cancel();
        sendPort.send(['ResetTimer']);
      }
    });
  }

My Runner Info.plist:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.myapp</string>
</array>

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>fetch</string>
    <string>processing</string>
    <string>remote-notification</string>
</array>
  • Apps do not run on iOS when the app is suspended. A timer that depends on constant execution will not work on iOS. A strategy is to keep a date when you start the timer and then you can simply calculate the elapsed time since that date. You do not need to actually run code when your app is onscreen. This is also better for battery. – Paulw11 Sep 30 '21 at 10:18
  • @Paulw11 Thanks for the messages. This is how it should work. – tharhtookyaw Sep 30 '21 at 10:54
  • @Paulw11 This is not true. Maybe these settings can help to run the background task in release mode: https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md – maxmitz Nov 23 '22 at 10:59
  • An iOS background task will let an app execute for a few seconds at undefined intervals (the most frequent you could expect is about every 15 minutes). It won't meet the OP needs – Paulw11 Nov 23 '22 at 11:06
  • @Paulw11 "The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week.". https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md . Spotify, Youtube, etc. are doing the same thing. Am I wrong? – maxmitz Nov 23 '22 at 11:13
  • No, but that is completely different to what the question is asking about. 30 seconds execution time sometime in the next two days isn't going to keep a timer ticking continually. – Paulw11 Nov 23 '22 at 11:44

0 Answers0