【问题标题】:How do I run code in the background, even with the screen off?即使屏幕关闭,如何在后台运行代码?
【发布时间】:2017-06-14 23:02:27
【问题描述】:

我在 Flutter 中有一个简单的计时器应用程序,它显示剩余秒数的倒计时。我有:

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

在我的手机显示屏关闭(即使我切换到另一个应用程序)并进入睡眠状态之前,它似乎工作正常。然后,计时器暂停。是否有推荐的方法来创建即使屏幕关闭也能在后台运行的服务?

【问题讨论】:

  • 也许真正的问题是:是否可以在 Activity 被销毁的情况下为 Flutter 应用程序在后台运行代码(例如计时器)?在我的情况下,即使我关闭显示器,计时器也会继续运行(请参阅下面的答案)。
  • 你不能在客户端完全做到这一点我认为你需要在服务器上运行一个计时器并将其与前端同步,比如数据流,这样当电话响起时睡眠并返回应用程序,然后它应该从服务器上的当前计时器开始。
  • 您也可以查看stackoverflow.com/a/59057145/6668797 了解其他方式

标签: android flutter


【解决方案1】:

简短回答:不,这是不可能的,尽管我观察到显示器进入睡眠状态时会出现不同的行为。以下代码将帮助您了解 Android 上 Flutter 应用的不同状态,并使用这些 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 秒递增一次。计数器下方的文本字段将显示 Flutter 应用的任何 AppLifecycleState 更改,您将在 Flutter 调试日志中看到相应的输出,例如:

[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

对于上面的日志输出,下面是我做的步骤:

  1. 使用flutter run 启动应用程序
  2. 切换到其他应用(_timerCounter 值为 1)
  3. 返回 Flutter 应用(_timerCounter 值为 3)
  4. 按下电源按钮,显示屏关闭(_timerCounter 值 4)
  5. 手机解锁,Flutter 应用恢复(_timerCounter 值为 7)
  6. 按下手机上的返回按钮(_timerCounter 值未更改)。这是 FlutterActivity 和 Dart VM Isolate 被破坏的时刻。
  7. Flutter 应用恢复(_timerCounter 值再次为 0)

在应用之间切换,按电源或返回按钮
当切换到另一个应用程序时,或者当按下电源按钮关闭屏幕时,计时器会继续运行。但是当 Flutter 应用程序获得焦点时按下后退按钮时,Activity 会被销毁,Dart 也会随之被隔离。您可以通过在应用程序之间切换或转动屏幕时连接到Dart Observatory 来进行测试。 Observatory 将显示一个活动的 Flutter 应用程序 Isolate 正在运行。但是当按下返回按钮时,天文台显示没有运行隔离。该行为已在运行 Android 6.x 的 Galaxy Note 5 和运行 Android 4.4.x 的 Nexus 4 上得到确认。

Flutter 应用生命周期和 Android 生命周期 对于 Flutter 小部件层,仅暴露了 pausedresumed 状态。 Android Flutter 应用的销毁由 Android Activity 处理:

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

由于 Flutter 应用的 Dart VM 在 Activity 中运行,因此每次 Activity 被销毁时 VM 都会停止。

Flutter Engine 代码逻辑
这不会直接回答您的问题,但会为您提供有关 Flutter 引擎如何处理 Android 状态更改的一些更详细的背景信息。
查看 Flutter 引擎代码很明显,当FlutterActivity 收到 Android 的Activity#onPause 事件时,动画循环会暂停。当应用程序进入 paused 状态时,根据source comment here 会发生以下情况:

“应用当前对用户不可见。当应用处于此状态时,引擎不会调用[onBeginFrame]回调。”

根据我的测试,即使 UI 渲染暂停,计时器也会继续工作,这是有道理的。当 Activity 被销毁时,最好使用 WidgetsBindingObserver 将事件发送到小部件层,这样开发人员可以确保在 Activity 恢复之前存储 Flutter 应用的状态。

【讨论】:

    【解决方案2】:

    回答如何实现您的特定计时器案例的问题实际上与后台代码无关。在移动操作系统上不鼓励在后台整体运行代码。

    例如,iOS 文档在这里更详细地讨论了背景代码: https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html

    相反,移动操作系统提供 api(如计​​时器/警报/通知 api)以在特定时间后回调您的应用程序。例如,在 iOS 上,您可以通过 UINotificationRequest 请求您的应用程序在未来的特定时间点得到通知/唤醒: https://developer.apple.com/reference/usernotifications/unnotificationrequest 这允许他们终止/暂停您的应用程序以实现更好的节能效果,而是拥有一个高效的共享系统服务来跟踪这些通知/警报/地理围栏等。

    Flutter 目前不提供任何开箱即用的操作系统服务封装,但是使用我们的平台服务模型编写您自己的封装是很简单的: flutter.io/platform-services

    我们正在开发一种用于发布/共享此类服务集成的系统,以便一旦有人编写此集成(例如安排您的应用的某些未来执行),每个人都可以受益。

    另外,“是否可以运行后台 Dart 代码”(没有在屏幕上激活 FlutterView)这个更普遍的问题是“还没有”。我们有一个文件错误: https://github.com/flutter/flutter/issues/3671 和一个未解决的问题:https://github.com/flutter/flutter/issues/32164

    驱动这种后台代码执行的用例是当您的应用收到通知时,想要使用一些 Dart 代码来处理它,而不是将您的应用置于前台。如果您希望我们了解其他后台代码用例,欢迎 cmets 解决该问题!

    【讨论】:

      【解决方案3】:

      您可以使用android_alarm_managerflutter 插件,它可以让您在警报触发时在后台运行 Dart 代码。

      另一种获得更多控制权的方法是为您的应用编写原生 Android service(使用 Java 或 Kotlin),通过设备存储或共享首选项与 Flutter 前端进行通信。

      【讨论】:

        【解决方案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());
        }
        

        【讨论】:

        • 周期性后台进程似乎在 iOS 上不起作用
        • 如何在使用 WorkManager 时检查位置权限,因为在 android 中未启用位置时似乎无法正常工作。
        【解决方案5】:

        我也遇到过同样的问题,我对这个特定案例(倒计时)的解决方案是使用与一些原生 android/ios 应用程序中使用的逻辑相同的逻辑,即:

        1. 当应用程序暂停(发送到后台)我存储结束日期时间 对象。
        2. 当应用程序恢复时(再次在前台)我重新计算持续时间 当前设备时间(Datetime.now()) 和存储的结束时间之间 日期时间对象。 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;
        
        

        然后在计时器屏幕内,我观察了应用程序的生命周期;

        • 所以一旦发送到后台(暂停)我会保存结束时间,
        • 一旦它再次进入前台(恢复),我将使用 新的剩余时间(而不是直接从新的持续时间开始, 您可以在发送到之前检查状态是暂停还是开始 背景,如果你需要的话)。

        注意事项:

        1- 在设置新的剩余持续时间之前,我不检查计时器状态, 因为我在我的应用程序中需要的逻辑是将endingTime推入 如果用户暂停了计时器,而不是减少 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;
            });
          }
        
        //....
        }
        

        如果有不清楚的地方,请发表评论。

        【讨论】:

        • 我想知道当应用程序进入后台时,单例模式中的数据会发生什么。
        【解决方案6】:

        我认为首先你需要防止系统在单击返回按钮时杀死 FlutterActivity

        您可以通过从 Flutter 调用原生 android 代码来实现 有一个名为 moveToBack(true) 的函数可以让你保持 FlutterActivity 运行。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-30
          • 1970-01-01
          • 1970-01-01
          • 2020-05-22
          • 1970-01-01
          相关资源
          最近更新 更多