「这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战」。
Flutter 应用中需要后台定时任务的话,可以使用 background_fetch 插件。
以下内容肉翻自 background_fetch 的 pub ,图片也来自相关的网站。
background_fetch | Flutter Package (pub.dev)
Flutter background_fetch
由 Flutter Background Geolocation 的创建者 Transistor Software 开发。
Background Fetch (后台获取)是一个大约每15分钟在后台唤醒 APP 、提供一个短暂的后台运行时间的简单插件。该插件每当 background-fetch 事件触发时都会运行你提供的 callbackFn (回调函数)。
🆕 Background Fetch 现在提供一个 scheduleTask (定时任务)方法来调度任何单次触发的任务或周期性的任务。
iOS
- 这里无法增加 fetch-event (获取事件)触发的频率 。该插件已尽可能将频率调至最频繁,但你永远不会接收到快于15分钟的事件。操作系统会根据用户的使用模式自动压制 background-fetch 事件触发的频率。 如:如果用户长时间没有使用手机,fetch-event 的触发就会不那么频繁。
scheduleTask像是只在接通电源时触发。scheduleTask是设计用于低优先度的任务,不会如期望中频繁运行。- 默认的
fetch任务会很频繁地运行。 - ⚠️ 当 APP 终止运行时,iOS 不会再触发事件 - 在 iOS 上没有
stopOnTerminate:false这样的设置。 - iOS 会在内置Apple 的机器学习算法适应之前运行几天任务然后开始有规律地触发事件。不用盯着你的 log 来等待事件触发,如果你已经模拟了事件运行,你需要知道的全部就是所有事情都已经正确配置。
- 如果用户很长时间没有打开 iOS 的应用,iOS 会停止触发事件。
Android
Android版的插件提供了一个 [Headless] pub.dartlang.org/documentati… (无头)的实现,允许即使 APP 终止之后也能继续处理事件。
内容
🔷 安装
📂 pubspec.yaml:
dependencies:
background_fetch: '^0.7.0'
或从 git 安装最新版:
dependencies:
background_fetch:
git:
url: https://github.com/transistorsoft/flutter_background_fetch
🔷 配置向导
iOS
配置 Background Capabilities
-
选中工程的根目录,选择 Capabilities 标签。Background Modes 及跟着后面的 Mode 设为可用。
- Background fetch
- Background processing (Only if you intend to use
BackgroundFetch.scheduleTask)
配置 Info.plist
- 打开
Info.plist,添加 Permitted background task scheduler identifiers 的 key。
- 添加需要的标识
com.transistorsoft.fetch
- 如果想通过
BackgroundFetch.scheduleTask执行自定义Task,也需要添加这些自定义标识。
例如:如果想执行一个taskId: 'com.transistorsoft.customtask'的自定义任务,也需要把com.transistorsoft.customtask标识添加到工程的 Permitted background task scheduler identifiers 中。
⚠️ 一个任务标识可以使用任何你想用的字符串,但是现在给这些标识符加上 com.transistorsoft. 前缀是一个好主意。 - 将来,com.transistorsoft 前缀可能会变成必需的。
BackgroundFetch.scheduleTask(TaskConfig(
taskId: 'com.transistorsoft.customtask',
delay: 60 * 60 * 1000 // 毫秒
));
Android
AndroidManifest
Flutter 在把第三方库合并进 Android 的 AndroidManifest.xml 里时好像有问题,特别是 android:label 属性。
📂 android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.helloworld">
<application
tools:replace="android:label"
android:name="io.flutter.app.FlutterApplication"
android:label="flutter_background_geolocation_example"
android:icon="@mipmap/ic_launcher">
</manifest>
上面的配置中,下面两行是需要添加的内容:
xmlns:tools="http://schemas.android.com/tools"
tools:replace="android:label"
⚠️ 上面的步骤操作失败的话会引发一个构建错误
Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : Attribute application@label value=(hello_world) from AndroidManifest.xml:17:9-36
is also present at [tslocationmanager-2.13.3.aar] AndroidManifest.xml:24:18-50 value=(@string/app_name).
Suggestion: add 'tools:replace="android:label"' to <application> element at AndroidManifest.xml:15:5-38:19 to override.
android/gradle.properties
确认应用已经迁移到使用 AndroidX。
📂 android/gradle.properties:
org.gradle.jvmargs=-Xmx1536M
android.enableJetifier=true
android.useAndroidX=true
上面的配置中,下面两行是需要添加的内容:
android.enableJetifier=true
android.useAndroidX=true
android/build.gradle
APP 会变得越来越复杂,会引入各种第三方模块,所以提供 "Global Gradle Configuration Properties" 会帮助所有模块来匹配它们请求的依赖版本。
background_fetch 会意识到这些变量,然后匹配它能检测到的依赖版本。
📂 android/build.gradle:
buildscript {
+ ext.kotlin_version = '1.3.72' // or latest
+ ext {
+ compileSdkVersion = 30 // or latest
+ targetSdkVersion = 30 // or latest
+ appCompatVersion = "1.1.0" // or latest
+ }
repositories {
google()
jcenter()
}
dependencies {
+ classpath 'com.android.tools.build:gradle:3.3.1' // Must use 3.3.1 or higher. 4.x is fine.
}
}
allprojects {
repositories {
google()
jcenter()
+ maven {
+ // [required] background_fetch
+ url "${project(':background_fetch').projectDir}/libs"
+ }
}
}
开头带 + (加号)的行是要添加的内容。
android/app/build.gradle
另外,需要自己来利用 Global Configuration Properties ,在 android/app/build.gradle 中用这些引用来替换硬编码的值。
📂 android/app/build.gradle:
android {
compileSdkVersion rootProject.ext.compileSdkVersion
.
.
.
defaultConfig {
.
.
.
targetSdkVersion rootProject.ext.targetSdkVersion
}
}
上面的配置中,下面两行是需要添加的内容:
compileSdkVersion rootProject.ext.compileSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
🔷 示例
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:background_fetch/background_fetch.dart';
// 仅 Android 如果 APP 以 enableHeadless: true 终止,Headless Task 会运行。
void backgroundFetchHeadlessTask(HeadlessTask task) async {
String taskId = task.taskId;
bool isTimeout = task.timeout;
if (isTimeout) {
// 如果任务已经超过了允许的运行时间,必须停止当前的处理并立即终止任务(.finish(taskId))
print("[BackgroundFetch] Headless task timed-out: $taskId");
BackgroundFetch.finish(taskId);
return;
}
print('[BackgroundFetch] Headless event received.');
// 在这里加你的处理
BackgroundFetch.finish(taskId);
}
void main() {
// 使用 Flutter 驱动扩展,可以集成测试。
// 更多信息参考:https://flutter.io/testing/
runApp(new MyApp());
// 注册任务,在 APP 终止后接收 BackgroundFetch 事件。
// 必需:{stopOnTerminate: false, enableHeadless: true}
BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _enabled = true;
int _status = 0;
List<DateTime> _events = [];
@override
void initState() {
super.initState();
initPlatformState();
}
// 平台消息是异步的,所以在异步方法里初始化
Future<void> initPlatformState() async {
// 配置 BackgroundFetch.
int status = await BackgroundFetch.configure(BackgroundFetchConfig(
minimumFetchInterval: 15,
stopOnTerminate: false,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: NetworkType.NONE
), (String taskId) async { // <-- 事件 Handler
// fetch 事件回调
print("[BackgroundFetch] Event received $taskId");
setState(() {
_events.insert(0, new DateTime.now());
});
// 重要: 你必须发出任务完成的信号或者 OS 会严控你的 APP 在后台长时间运行。
BackgroundFetch.finish(taskId);
}, (String taskId) async { // <-- 任务超时 Handler
// 任务超过了允许的运行时间。必须停止当前的处理并立即终止任务(.finish(taskId))
print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId");
BackgroundFetch.finish(taskId);
});
print('[BackgroundFetch] configure success: $status');
setState(() {
_status = status;
});
// 如果异步平台消息还在处理中时组件从组件树中被移除,我们会放弃响应,而不是调用 setState 来更新已不存在的内容。
if (!mounted) return;
}
void _onClickEnable(enabled) {
setState(() {
_enabled = enabled;
});
if (enabled) {
BackgroundFetch.start().then((int status) {
print('[BackgroundFetch] start success: $status');
}).catchError((e) {
print('[BackgroundFetch] start FAILURE: $e');
});
} else {
BackgroundFetch.stop().then((int status) {
print('[BackgroundFetch] stop success: $status');
});
}
}
void _onClickStatus() async {
int status = await BackgroundFetch.status;
print('[BackgroundFetch] status: $status');
setState(() {
_status = status;
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('BackgroundFetch Example', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.amberAccent,
brightness: Brightness.light,
actions: <Widget>[
Switch(value: _enabled, onChanged: _onClickEnable),
]
),
body: Container(
color: Colors.black,
child: new ListView.builder(
itemCount: _events.length,
itemBuilder: (BuildContext context, int index) {
DateTime timestamp = _events[index];
return InputDecorator(
decoration: InputDecoration(
contentPadding: EdgeInsets.only(left: 10.0, top: 10.0, bottom: 0.0),
labelStyle: TextStyle(color: Colors.amberAccent, fontSize: 20.0),
labelText: "[background fetch event]"
),
child: new Text(timestamp.toString(), style: TextStyle(color: Colors.white, fontSize: 16.0))
);
}
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
children: <Widget>[
RaisedButton(onPressed: _onClickStatus, child: Text('Status')),
Container(child: Text("$_status"), margin: EdgeInsets.only(left: 20.0))
]
)
),
),
);
}
}
执行自定义任务
除了 BackgrondFetch.configure 中定义的默认 background-fetch 任务之外,还可以执行任何属于你的单次运行任务或周期性的任务( iOS 需要额外配置)。无论如何,所有的事件都会触发 **BackgroundFetch#configure** 中提供的回调:
⚠️ iOS:
scheduleTask在 iOS 上只在设备接通电源时运行。scheduleTask在 iOS 上设计用于低优先度的任务,如清理缓存文件 - 对于关键任务来说是不可靠的。scheduleTask永远不会如期望中运行频繁。- 默认的
fetch事件更可靠,触发也更频繁。 - 在 iOS 上,用户终止 APP 时,
scheduleTask也会终止。在 iOS 上没有stopOnTerminate: false的设置。
// 第一步:(和通常情况一样)配置BackgroundFetch
int status = await BackgroundFetch.configure(BackgroundFetchConfig(
minimumFetchInterval: 15
), (String taskId) async { // <-- 事件回调
// 这是一个 fetch 事件回调
print("[BackgroundFetch] taskId: $taskId");
// 使用 switch 语句为任务调度提供路由。
switch (taskId) {
case 'com.transistorsoft.customtask':
print("Received custom task");
break;
default:
print("Default fetch task");
}
// 完成,提供接收的任务 ID
BackgroundFetch.finish(taskId);
}, (String taskId) async { // <-- 事件超时回调
// 任务已经超过允许的运行时间。需要停止当前的处理并立即终止任务(.finish(taskId))。
print("[BackgroundFetch] TIMEOUT taskId: $taskId");
BackgroundFetch.finish(taskId);
});
// 第二步:制定一个从现在执行5000毫秒 "com.transistorsoft.customtask" 的自定义单次任务。
BackgroundFetch.scheduleTask(TaskConfig(
taskId: "com.transistorsoft.customtask",
delay: 5000 // <-- 毫秒
));
🔷 调试
iOS
🆕 在 iOS 13+ 中为BGTaskScheduler 模拟事件
-
在本文(pub 文档)编写时,新的任务模拟器还不能在模拟器中运行,只能真机中运行。
-
在 XCode 中运行 APP 之后,点击
[||]按钮来初始化断点。 -
在控制台(
lldb)粘贴下面的命令(注: 在光标处用上/下键可调出前面运行过的命令):e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.transistorsoft.fetch"] -
点击
[>]按钮继续。任务会执行,然后提供给**`BackgroundFetch.configure`**的回调函数会接收事件。
红字内容:通过改变标识来模拟任何已注册的任务。
模拟任务超时事件
-
只有新的
BGTaskSchedulerAPI支持模拟 任务超时事件。要模拟一个任务超时,fetchCallback(fetch 回调)必须不调用BackgroundFetch.finish(taskId):BackgroundFetch.configure(BackgroundFetchConfig( minimumFetchInterval: 15 ), (String taskId) async { // <-- 事件回调 // 这是 fetch 事件回调 print("[BackgroundFetch] taskId: $taskId"); //BackgroundFetch.finish(taskId); // <-- 模拟 iOS 任务超时时禁用 .finish(任务Id) }, (String taskId) async { // <-- 事件超时回调 // 任务已经超过允许的运行时间。需要停止当前的处理并立即终止任务(.finish(taskId))。 print("[BackgroundFetch] TIMEOUT taskId: $taskId"); BackgroundFetch.finish(taskId); }); -
现在如下模拟一个 iOS 任务超时,和上面同样地模拟一个事件
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.transistorsoft.fetch"]
旧 BackgroundFetch API
- 在 XCode 中使用
Debug->Simulate Background Fetch模拟后台 fetch 事件。 - iOS 可能会花几个小时甚至几天的时间来开启一个持续的后台 fetch 事件调度。因为 iOS 基于用户的使用模式来调度 fetch 事件。如果 Simulate Background Fetch 开始运行,就可以确认所有事情都运行正常,只是需要等待。
Android
-
在
$abd logcat查看下插件日志。$ adb logcat *:S flutter:V, TSBackgroundFetch:V -
在设备上模拟一个 background-fetch 事件(insert <your.application.id>)只在 sdk21+ 的版本中可运行。
$ adb shell cmd jobscheduler run -f <your.application.id> 999 -
低于 sdk21 的版本,模拟一个 “Headless” 事件(使用 insert <your.application.id>)
$ adb shell am broadcast -a <your.application.id>.event.BACKGROUND_FETCH
Demo 应用
插件仓库里有一个 /example 目录,Clone 仓库并在 Android Studio 中打开 /example 目录。
🔷 实现
iOS
实现了 performFetchWithCompletionHandler, 触发在 cordova 插件中订阅的自定义事件。
Android
Android 根据不同的 Android SDK 版本使用两个不同的机制来实现 background fetch 。 LOLLIPOP及更高版本使用新的 JobScheduler ,否则使用旧的 AlarmManager 。
不像 iOS,Android 上的实现能够在应用终止( stopOnTerminate: false)或者设备重启( startOnBoot: true)后继续操作
🔷 许可证
MIT 许可证
详细参照 该插件的 pub。