我在 Flutter 中有一个简单的计时器应用程序,它显示剩余秒数的倒计时。我有:
new Timer.periodic(new Duration(seconds: 1), _decrementCounter);
在我的手机显示屏关闭(即使我切换到另一个应用程序)并进入睡眠状态之前,它似乎工作正常。然后,计时器暂停。是否有推荐的方法来创建即使屏幕关闭也能在后台运行的服务?
回答如何实现您的特定计时器案例的问题实际上与后台代码无关。在移动操作系统上不鼓励在后台运行整体代码。
例如,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 目前没有提供任何开箱即用的 OS 服务包装器,但是可以直接使用我们的平台服务模型编写自己的包装器:flutter.io/platform-services
我们正在开发一个用于发布/共享此类服务集成的系统,以便一旦有人编写此集成(例如安排您的应用程序的某些未来执行),每个人都可以受益。
另外,“是否可以运行后台 Dart 代码”(没有在屏幕上激活 FlutterView)这个更普遍的问题是“还没有”。我们有一个错误存档: https ://github.com/flutter/flutter/issues/3671和一个未解决的问题:https ://github.com/flutter/flutter/issues/32164
驱动这种后台代码执行的用例是当您的应用程序收到通知时,想要使用一些 Dart 代码处理它而不将您的应用程序置于最前面。如果您希望我们了解其他后台代码用例,欢迎对该错误发表评论!
简短的回答:不,这是不可能的,尽管我观察到显示器进入睡眠状态的不同行为。以下代码将帮助您了解 Android 上 Flutter 应用程序的不同状态,并使用这些 Flutter 和 Flutter Engine 版本进行测试:
创建一个新的 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
对于上述日志输出,以下是我执行的步骤:
flutter run
在应用程序之间切换,按电源或返回按钮
切换到另一个应用程序时,或按电源按钮关闭屏幕时,计时器会继续运行。但是当 Flutter 应用程序获得焦点时按下后退按钮时,Activity 会被销毁,Dart 也会随之被隔离。您可以通过在应用程序之间切换或转动屏幕时连接到Dart Observatory来进行测试。Observatory 将显示一个活动的 Flutter 应用程序 Isolate 正在运行。但是当按下返回按钮时,天文台显示没有运行隔离。该行为在运行 Android 6.x 的 Galaxy Note 5 和运行 Android 4.4.x 的 Nexus 4 上得到确认。
Flutter 应用生命周期和 Android 生命周期 对于 Flutter 小部件层,仅公开暂停和恢复状态。销毁由Android Activity为 Android Flutter 应用处理:
/**
* @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事件时,动画循环会暂停。当应用程序进入暂停状态时,根据此处的源注释,会发生以下情况:
“应用程序当前对用户不可见。当应用程序处于此状态时,引擎将不会调用 [onBeginFrame] 回调。”
根据我的测试,即使 UI 渲染暂停,计时器也会继续工作,这是有道理的。当 Activity 被销毁时,最好使用WidgetsBindingObserver将事件发送到小部件层,这样开发人员可以确保存储 Flutter 应用程序的状态,直到 Activity 恢复。
我遇到了同样的问题,我对这个特定情况(倒数计时器)的解决方案是使用与一些本地 android/ios 应用程序中使用的逻辑相同的逻辑,即:
Duration remainingTime = _endingTime.difference(dateTimeNow);
注意:结束日期时间值已存储在单例中,我没有使用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-在设置新的剩余持续时间之前我不检查计时器状态,因为我的应用程序中需要的逻辑是在用户暂停计时器的情况下推送结束时间,而不是减少计时器持续时间,完全取决于用例.
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;
});
}
//....
}
如果有不清楚的地方,请发表评论。
您可以使用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());
}
您可以使用android_alarm_manager颤振插件,它可以让您在警报触发时在后台运行 Dart 代码。
另一种具有更多控制权的方法是为您的应用程序编写一个原生Android 服务(使用 Java 或 Kotlin),该服务通过设备存储或共享首选项与 Flutter 前端进行通信。
我认为首先你需要防止系统在单击返回按钮时杀死 FlutterActivity
您可以通过从 Flutter 调用本机 android 代码来实现这一点,有一个名为moveToBack(true)的函数允许您保持 FlutterActivity 运行。