即使关闭屏幕,如何在后台运行代码?

时间:2017-01-29 19:15:13

标签: android flutter

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

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

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

6 个答案:

答案 0 :(得分:22)

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

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

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

Flutter目前不提供开箱即用的这些OS服务的任何包装器,但是使用我们的平台服务模型编写自己的包装是很简单的: flutter.io/platform-services

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

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

用户驱动那种背景代码执行是在您的应用收到通知时,想要使用一些Dart代码处理它而不将您的应用程序带到前面。如果您希望我们了解有关后台代码的其他用例,欢迎提出有关该错误的评论!

答案 1 :(得分:17)

简短的回答:不,它不可能,虽然我已经观察到显示器进入睡眠的不同行为。以下代码将帮助您了解Android上Flutter应用程序的不同状态,使用这些Flutter和Flutter引擎版本进行测试:

  • 框架修订版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. 启动应用
  3. 切换到其他应用(_timerCounter值1)
  4. 返回Flutter app(_timerCounter value 3)
  5. 按下电源按钮,显示屏关闭(_timerCounter value 4)
  6. 解锁手机,Flutter应用恢复(_timerCounter值7)
  7. 按下电话按钮(_timerCounter值未更改)。这是FlutterActivity被破坏的时刻,也是Dart VM Isolate。
  8. Flutter app恢复(_timerCounter值再次为0)
  9. 在应用之间切换,按电源或后退按钮
    当切换到另一个应用程序,或按下电源按钮以关闭屏幕时,计时器继续运行。但是当Flutter应用程序具有焦点时按下后退按钮,活动会被破坏,并且随之而来的是Dart隔离。您可以在应用程序之间切换或转动屏幕时连接到Dart Observatory来测试。天文台将展示一个活跃的Flutter应用程序隔离运行。但是当按下后退按钮时,天文台显示没有运行隔离。在运行Android 6.x的Galaxy Note 5和运行Android 4.4.x的Nexus 4上确认了这一行为。

    展开应用生命周期和Android生命周期 对于Flutter窗口小部件图层,仅公开已暂停已恢复状态。对于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事件时,动画循环会暂停。当应用程序进入暂停状态时,根据source comment here发生以下情况:

    &#34;用户当前无法看到该应用程序。当应用程序处于此状态时,引擎将不会调用[onBeginFrame]回调。&#34;

    根据我的测试,即使暂停UI渲染,计时器也会继续工作,这是有道理的。当Activity被销毁时,使用 WidgetsBindingObserver 将事件发送到窗口小部件层会很好,因此开发人员可以确保存储Flutter应用程序的状态,直到活动恢复为止。

答案 2 :(得分:1)

您可以使用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());
}

答案 3 :(得分:1)

我认为首先您需要避免单击后退按钮时系统杀死FlutterActivity

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

答案 4 :(得分:0)

您可以使用android_alarm_manager flutter插件,该插件可以在警报触发时在后台运行Dart代码。

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

答案 5 :(得分:0)

我遇到了同样的问题,我针对此特定情况(倒数计时器)的解决方案是使用与某些本机android / ios Apps相同的逻辑,即:

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

//....
}

以防万一,请发表评论。