Как запустить код в фоновом режиме, даже если экран выключен?

У меня есть приложение таймера в Flutter, которое показывает обратный отсчет с оставшимся количеством секунд. У меня есть:

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

Кажется, что он работает нормально, пока мой дисплей телефона не выключится (даже если я переключусь на другое приложение) и перейдет спать. Затем таймер приостанавливается. Есть ли рекомендуемый способ создания службы, которая работает в фоновом режиме, даже когда экран выключен?

Ответ 1

Ответ на вопрос о том, как реализовать свой конкретный таймер, на самом деле не связан с кодом фона. Общий бегущий код в фоновом режиме является чем-то обескураженным для мобильных операционных систем.

Например, в документации iOS более подробно обсуждается фоновый код: https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html

Вместо этого мобильные операционные системы предоставляют apis (например, таймер/будильник/уведомление apis), чтобы перезвонить в ваше приложение через определенное время. Например, на iOS вы можете запросить, чтобы ваше приложение было уведомлено/пробуждено в определенный момент в будущем через UINotificationRequest: https://developer.apple.com/reference/usernotifications/unnotificationrequest. Это позволяет им убивать/приостанавливать ваше приложение для достижения более эффективную экономию энергии и вместо этого имеют единую высокоэффективную общую системную услугу для отслеживания этих уведомлений/аварийных сигналов/геообъектов и т.д.

В настоящее время Flutter не предоставляет каких-либо оберток вокруг этих служб ОС, но очень сложно писать свои собственные, используя нашу платформу-сервисную модель: flutter.io/platform-services

Мы работаем над системой публикации/совместного использования сервисов таким образом, чтобы, как только один человек записывал эту интеграцию (например, планируя какое-то будущее исполнение вашего приложения), каждый может выиграть.

Отдельно, более общий вопрос: "возможно ли запустить фоновый код Дарта" (без активного FlutterView на экране), "еще не". У нас есть ошибка в файле: https://github.com/flutter/flutter/issues/3671

Использование прецедентов, выполняющих такой код, когда ваше приложение получает уведомление, хочет обработать его с помощью какого-либо кода Dart, не принося ваше приложение на передний план. Если у вас есть другие варианты использования фонового кода, о которых вы хотели бы узнать, комментарии наиболее приветствуются в этой ошибке!

Ответ 2

Короткий ответ: нет, это невозможно, хотя я наблюдал другое поведение, пока дисплей не заснул. Следующий код поможет вам понять различные состояния приложения Flutter на Android, протестированные с этими версиями Flutter и Flutter Engine:

  • Рамочная версия b339c71523 (6 часов назад), 2017-02-04 00:51:32
  • Версия двигателя cd34b0ef39

Создайте новое приложение Flutter и замените содержимое lib/main.dart на этот код:

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(),
        ],
      ),
    );
  }
}

При запуске приложения значение _timerCounter увеличивается каждые 3 секунды. Текстовое поле под счетчиком отображает изменения AppLifecycleState для приложения Flutter, вы увидите соответствующий вывод в журнале отладки Flutter, например:

[[email protected]:~/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

Для приведенного выше выходного журнала выполните следующие шаги:

  • Запустите приложение с flutter run
  • Переключитесь на другое приложение (значение _timerCounter 1)
  • Возврат в приложение Flutter (значение _timerCounter 3)
  • Нажмите кнопку питания, дисплей выключен (значение _timerCounter 4)
  • Разблокированный телефон, приложение Flutter возобновлено (значение _timerCounter 7)
  • Отключенная кнопка на телефоне (значение _timerCounter не изменено). Это момент, когда FlutterActivity уничтожается и Dart VM Isolate также.
  • Приложение флаттера возобновлено (значение _timerCounter равно 0)

Переключение между приложениями, нажатие кнопки "Питание" или "Назад"
При переключении на другое приложение или при нажатии кнопки питания для поворота экрана таймер продолжает работать. Но при нажатии кнопки "Назад", когда приложение "Флаттер" имеет фокус, "Активность" уничтожается, а вместе с ней выделяется дротик. Вы можете проверить это, подключившись к

Жизненный цикл приложения Flutter и жизненный цикл Android Для слоя виджетов Flutter отображаются только приостановленные и возобновленные состояния. Destroy обрабатывается для приложения Android Flutter:

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

Так как Dart VM для приложения Flutter запущена внутри Activity, VM будет остановлена ​​каждый раз, когда действие будет уничтожено.

Логика кода двигателя флаттера
Это напрямую не отвечает на ваш вопрос, но даст вам более подробную информацию о том, как движок Flutter обрабатывает изменения состояния для Android.
Просматривая код двигателя Flutter, становится очевидным, что цикл анимации приостановлен, когда получает Android . Когда приложение переходит в состояние паузы, в соответствии с происходит следующее:

"Приложение не отображается в данный момент пользователю. Когда приложение находится в этом состоянии, движок не будет вызывать обратный вызов [onBeginFrame]".

Основываясь на моем тестировании, таймер продолжает работать даже при приостановке отображения пользовательского интерфейса, что имеет смысл. Было бы неплохо отправить событие в слой виджетов с помощью WidgetsBindingObserver, когда активность будет уничтожена, поэтому разработчики могут следить за сохранением состояния приложения Flutter до тех пор, пока активность не будет возобновлена.

Ответ 3

Вы можете использовать плагина плагина android_alarm_manager, который позволяет запускать код Dart в фоновом режиме при срабатывании будильника.

Другим способом с большим контролем было бы написать собственный сервис Android (с использованием Java или Kotlin) для вашего приложения, который обменивается данными с интерфейсом флаттера через хранилище устройства или общие префы.

Ответ 4

Вы можете использовать плагин flutter_workmanager.
Это лучше, чем вышеупомянутое AlarmManager, так как это больше не рекомендуется для Android.
Плагин также всегда для фонового выполнения iOS

Этот плагин позволяет вам зарегистрировать некоторую фоновую работу и получить обратный вызов в Dart, когда это произошло, чтобы вы могли выполнить пользовательское действие.

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());
}

Ответ 5

Я столкнулся с той же проблемой, и мое решение этого конкретного случая (таймер обратного отсчета) заключалось в том, чтобы использовать ту же логику, что и в некоторых родных приложениях для Android/IOS, а именно:

  1. Когда приложение приостановлено (отправить в фоновый режим), я сохраняю окончание DateTime объект.
  2. Когда приложение возобновилось (снова на переднем плане), я пересчитал продолжительность между текущим временем устройства (Datetime.now()) и сохраненным окончанием Datetime объект. Duration remainingTime = _endingTime.difference(dateTimeNow);
  3. Обновите значение таймера обратного отсчета, указав новую продолжительность.

ПРИМЕЧАНИЕ. Конечное значение даты и времени было сохранено в одиночном теге. не использовал SharedPreferences, поскольку в моем случае нет необходимости, но это приемлемый вариант, если вам это нужно.

в деталях:

Я создал этот обработчик для установки и получения оставшегося времени:

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;

затем на экране таймера я посмотрел жизненный цикл приложения;

  • поэтому после отправки в фоновый режим (пауза) я сохраню время окончания,
  • и как только он снова на переднем плане (возобновил), я запускаю таймер с новое оставшееся время (вместо непосредственного начала с новой продолжительностью, Вы можете проверить, было ли состояние приостановлено или запущено перед отправкой фон, если вам это нужно).

NOTES:

1- Я не проверяю состояние таймера перед установкой новой оставшейся продолжительности, потому что логика, которая мне нужна в моем приложении, состоит в том, чтобы выдвинуть endTime в случай, когда пользователь приостановил таймер, вместо уменьшения timerDuration, полностью до варианта использования.

2- Мой таймер живет в блоке (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;
    });
  }

//....
}

если что-то не понятно, просто оставьте комментарий.