How do I run code in the background, even with the screen off?

I have a simple timer app in Flutter, which shows a countdown with the number of seconds remaining. I have:

new Timer.periodic(new Duration(seconds: 1), _decrementCounter);

It seems to work fine until my phone's display switches off (even if I switch to another app) and goes to sleep. Then, the timer pauses. Is there a recommended way to create a service that runs in the background even when the screen is off?


Answering the question of how to implement your specific timer case doesn't actually have to do with background code. Overall running code in the background is something discouraged on mobile operating systems.

For example, iOS Documentation discusses background code in greater detail here: https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html

Instead mobile operating systems provide apis (like a timer/alarm/notification apis) to call back to your application after a specific time. For example on iOS you can request that your application be notified/woken at a specific point in the future via UINotificationRequest: https://developer.apple.com/reference/usernotifications/unnotificationrequest This allows them to kill/suspend your app to achieve better power savings and instead have a single highly-efficent shared system service for tracking these notifications/alarms/geofencing, etc.

Flutter does not currently provide any wrappers around these OS services out-of-the-box, however it is straighforward to write your own using our platform-services model: flutter.io/platform-services

We're working on a system for publishing/sharing service integrations like this so that once one person writes this integration (for say scheduling some future execution of your app) everyone can benefit.

Separately, the more general question of "is it possible to run background Dart code" (without having a FlutterView active on screen), is "not yet". We have a bug on file: https://github.com/flutter/flutter/issues/3671 and an open issue: https://github.com/flutter/flutter/issues/32164

The use-case driving that kind of back-ground code execution is when your app receives a notification, wants to process it using some Dart code without bringing your app to the front. If you have other use cases for background code you'd like us to know about, comments are most welcome on that bug!


Short answer: no, it's not possible, although I have observed a different behavior for the display going to sleep. The following code will help you understand the different states of a Flutter app on Android, tested with the these Flutter and Flutter Engine versions:

  • Framework revision b339c71523 (6 hours ago), 2017-02-04 00:51:32
  • Engine revision cd34b0ef39

Create a new Flutter app, and replace the content of lib/main.dart with this code:

import 'dart:async';

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => new _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State<LifecycleWatcher>
    with WidgetsBindingObserver {
  AppLifecycleState _lastLifecyleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void onDeactivate() {
    super.deactivate();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print("LifecycleWatcherState#didChangeAppLifecycleState state=${state.toString()}");
    setState(() {
      _lastLifecyleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecyleState == null)
      return new Text('This widget has not observed any lifecycle changes.');
    return new Text(
        'The most recent lifecycle state this widget observed was: $_lastLifecyleState.');
  }
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyHomePage(title: 'Flutter App Lifecycle'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _timerCounter = 0;
  // ignore: unused_field only created once
  Timer _timer;

  _MyHomePageState() {
    print("_MyHomePageState#constructor, creating new Timer.periodic");
    _timer = new Timer.periodic(
        new Duration(milliseconds: 3000), _incrementTimerCounter);
  }

  void _incrementTimerCounter(Timer t) {
    print("_timerCounter is $_timerCounter");
    setState(() {
      _timerCounter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(config.title),
      ),
      body: new Block(
        children: [
          new Text(
            'Timer called $_timerCounter time${ _timerCounter == 1 ? '' : 's' }.',
          ),
          new LifecycleWatcher(),
        ],
      ),
    );
  }
}

When launching the app, the value of _timerCounter is incremented every 3s. A text field below the counter will show any AppLifecycleState changes for the Flutter app, you will see corresponding output in the Flutter debug log, e.g.:

[raju@eagle:~/flutter/helloworld]$ flutter run
Launching lib/main.dart on SM N920S in debug mode...
Building APK in debug mode (android-arm)...         6440ms
Installing build/app.apk...                         6496ms
I/flutter (28196): _MyHomePageState#constructor, creating new Timer.periodic
Syncing files to device...
I/flutter (28196): _timerCounter is 0

🔥  To hot reload your app on the fly, press "r" or F5. To restart the app entirely, press "R".
The Observatory debugger and profiler is available at: http://127.0.0.1:8108/
For a more detailed help message, press "h" or F1. To quit, press "q", F10, or Ctrl-C.
I/flutter (28196): _timerCounter is 1
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 2
I/flutter (28196): _timerCounter is 3
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.resumed
I/flutter (28196): _timerCounter is 4
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 5
I/flutter (28196): _timerCounter is 6
I/flutter (28196): _timerCounter is 7
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.resumed
I/flutter (28196): LifecycleWatcherState#didChangeAppLifecycleState state=AppLifecycleState.paused
I/flutter (28196): _timerCounter is 8
I/flutter (28196): _MyHomePageState#constructor, creating new Timer.periodic
I/flutter (28196): _timerCounter is 0
I/flutter (28196): _timerCounter is 1

For the above log output, here are the steps I did:

  1. Launch app with flutter run
  2. Switch to other app (_timerCounter value 1)
  3. Return to Flutter app (_timerCounter value 3)
  4. Pressed power button, display turned off (_timerCounter value 4)
  5. Unlocked phone, Flutter app resumed (_timerCounter value 7)
  6. Pressed back button on phone (_timerCounter value not changed). This is the moment where the FlutterActivity gets destroyed and the Dart VM Isolate as well.
  7. Flutter app resumed (_timerCounter value is 0 again)

Switching between apps, pressing power or back button
When switching to another app, or when pressing the power button to turn of the screen the timer continues to run. But when pressing the back button while the Flutter app has the focus, the Activity gets destroyed, and with it the Dart isolate. You can test that by connecting to the Dart Observatory when switching between apps, or turning of the screen. The Observatory will show an active Flutter app Isolate running. But when pressing the back button, the Observatory shows no running Isolate. The behavior was confirmed on a Galaxy Note 5 running Android 6.x, and a Nexus 4 running Android 4.4.x.

Flutter app lifecycle and Android lifecycle For the Flutter widget layer, only the paused and resumed states are exposed. Destroy is handled by Android Activity for an Android Flutter app:

/**
 * @see android.app.Activity#onDestroy()
 */
@Override
protected void onDestroy() {
    if (flutterView != null) {
        flutterView.destroy();
    }
    super.onDestroy();
}

Since the Dart VM for a Flutter app is running inside the Activity, the VM will be stopped every time the Activity gets destroyed.

Flutter Engine code logic
This doesn't directly answer your question, but will give you some more detailed background info on how the Flutter engine handles state changes for Android.
Looking through the Flutter engine code it becomes obvious that the animation loop is paused when the FlutterActivity receives the Android Activity#onPause event. When the application goes into paused state, according to the source comment here the following happens:

"The application is not currently visible to the user. When the application is in this state, the engine will not call the [onBeginFrame] callback."

Based on my testing the timer continues to work even with the UI rendering being paused, which makes sense. It would be good to send an event into the widget layer using the WidgetsBindingObserver when the Activity gets destroyed, so developers can make sure to store the state of the Flutter app until the Activity is resumed.


I have faced the same problem and my solution to this specific case (countdown timer) was to use same logic as used in some native android/ios Apps out there, which is:

  1. When App paused(send to background) I store the ending DateTime object.
  2. When App resumed(in foreground again) I recalculate the the duration between current device time(Datetime.now()) and the stored ending Datetime object. Duration remainingTime = _endingTime.difference(dateTimeNow);
  3. Update the countdown timer value with the new duration.

NOTE: Ending datetime value has been stored in a singleton, I didn't use SharedPreferences as no need in my case but it's an acceptable option in case you needed it.

in details:

I have created this handler to set and get remaining time:

class TimerHandler {
  DateTime _endingTime;

  TimerHandler._privateConstructor();
  TimerHandler();

  static final TimerHandler _instance = new TimerHandler();
  static TimerHandler get instance => _instance;

  int get remainingSeconds {
    final DateTime dateTimeNow = new DateTime.now();
    Duration remainingTime = _endingTime.difference(dateTimeNow);
    // Return in seconds
    return remainingTime.inSeconds;
  }

  void setEndingTime(int durationToEnd) {
    final DateTime dateTimeNow = new DateTime.now();

    // Ending time is the current time plus the remaining duration.
    this._endingTime = dateTimeNow.add(
      Duration(
        seconds: durationToEnd,
      ),
    );

  }
}
final timerHandler = TimerHandler.instance;

then inside timer screen, I watched the app's life cycle;

  • so once send to background(paused) I will save the ending time,
  • and once it in foreground(resumed) again, I start the timer with the new remaining time(Instead of directly start with the new duration, you can check if the state was paused or started before send to background, if you need that).

NOTES:

1- I don't check the timer state before set new remaining duration, because the logic that I need in my App is to push the endingTime in case the user paused the timer, instead of reduce the timerDuration, totally up to the use case.

2- My timer lives in a bloc( TimerBloc).

class _TimerScreenState extends State<TimerScreen> {
  int remainingDuration;
//...

  @override
  void initState() {
    super.initState();

    SystemChannels.lifecycle.setMessageHandler((msg) {

      if (msg == AppLifecycleState.paused.toString() ) {
        // On AppLifecycleState: paused
        remainingDuration = BlocProvider.of<TimerBloc>(context).currentState.duration ?? 0;
        timerHandler.setEndingTime(remainingDuration);
        setState((){});
      }

      if (msg == AppLifecycleState.resumed.toString() ) {
        // On AppLifecycleState: resumed
        BlocProvider.of<TimerBloc>(context).dispatch(
          Start(
            duration: timerHandler.remainingSeconds,
          ),
        );
        setState((){});
      }
      return;
    });
  }

//....
}

in case something's not clear just leave a comment.


You could use the flutter_workmanager plugin.
It's better than the above mentioned AlarmManager since this is not recommended any more for Android.
The plugin also always for iOS background execution

This plugin allows you do register some background work and get a callback in Dart when it happened so you can perform a custom action.

void callbackDispatcher() {
  Workmanager.executeTask((backgroundTask) {
    switch(backgroundTask) {
      case Workmanager.iOSBackgroundTask:
      case "firebaseTask":
        print("You are now in a background Isolate");
        print("Do some work with Firebase");
        Firebase.doSomethingHere();
        break;
    }
    return Future.value(true);
  });
}

void main() {
  Workmanager.initialize(callbackDispatcher);
  Workmanager.registerPeriodicTask(
    "1",
    "firebaseTask",
    frequency: Duration(days: 1),
    constraints: WorkManagerConstraintConfig(networkType: NetworkType.connected),
  );
  runApp(MyApp());
}