flutter_foreground_task 패키지는 Android에서 포그라운드 서비스를 생성하고 관리할 수 있게 해주며, 앱이 백그라운드에 있을 때도 장시간 실행되는 작업이 계속되도록 보장합니다. 사용자 정의 알림을 포함한 포그라운드 작업을 시작, 중지, 모니터링할 수 있는 간단한 API를 제공합니다. 주로 Android용으로 설계되었지만, 제한된 기능으로 iOS도 지원합니다. 음악 플레이어, 위치 추적, 백그라운드 데이터 동기화와 같은 지속적인 작동이 필요한 앱에 유용합니다.
사용 사례
백그라운드 오디오 재생
위치 추적 서비스
장시간 실행되는 데이터 동기화
실시간 모니터링
지속적인 작업 실행
주요 기능
Android 포그라운드 서비스 지원
사용자 정의 알림 표시
작업 라이프사이클 관리
크로스 플랫폼 호환성(아이오스에서 제한적)
간단한 API 통합
적합한 대상
지속적인 백그라운드 실행이 필요한 앱
미디어 또는 추적 앱을 개발하는 개발자
신뢰할 수 있는 프론트그라운드 작업 처리가 필요한 프로젝트
안드로이드 우선 모바일 솔루션을 목표로 하는 팀
고려 사항
제한된 iOS 기능
포그라운드 서비스를 위해 사용자 권한이 필요합니다
작업 실행 중에 알림이 보여야 합니다
배터리 수명에 영향을 줄 수 있습니다
단기적인 백그라운드 작업에 적합하지 않습니다
원본 README
연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.
This plugin is used to implement a foreground service on the Android platform.
Features
Can perform repetitive tasks with the foreground service.
Supports two-way communication between the foreground service and UI(main isolate).
Provides a widget that minimize the app without closing it when the user presses the soft back button.
Provides useful utilities that can use while performing tasks.
Provides an option to automatically resume the foreground service on boot.
After adding the plugin to your flutter project, we need to declare the platform-specific permissions ans service to use for this plugin to work properly.
:baby_chick: Android
This plugin requires Kotlin version 2.2.20+ and Gradle version 8.11.1+. Please refer to the migration documentation for more details.
Open the AndroidManifest.xml file and declare the service tag inside the <application> tag as follows.
If you want the foreground service to run only when the app is running, add android:stopWithTask="true".
As mentioned in the Android guidelines, to start a FG service on Android 14+, you must declare android:foregroundServiceType.
<!-- required -->
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<!-- foregroundServiceType: dataSync -->
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- foregroundServiceType: remoteMessaging -->
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<!-- Warning: Do not change service name. -->
<serviceandroid:name="com.pravera.flutter_foreground_task.service.ForegroundService"android:foregroundServiceType="dataSync|remoteMessaging"android:exported="false" />
[!CAUTION]
Check runtime requirements before starting the service. If this requirement is not met, the foreground service cannot be started.
[!CAUTION]
Android 15 introduces a new timeout behavior to dataSync for apps targeting Android 15 (API level 35) or higher.
The system permits an app's dataSync services to run for a total of 6 hours in a 24-hour period.
However, if the user brings the app to the foreground, the timer resets and the app has 6 hours available.
There are new restrictions on BOOT_COMPLETED(autoRunOnBoot) broadcast receivers launching foreground services.
BOOT_COMPLETED receivers are not allowed to launch the following types of foreground services:
To use this plugin developed in Swift in a project using Objective-C, you need to add a bridge header.
If there is no ios/Runner/Runner-Bridging-Header.h file in your project, check this page.
Open the ios/Runner/AppDelegate.swift file and add the commented code.
Initialize port for communication between TaskHandler and UI.
voidmain() {
// Initialize port for communication between TaskHandler and UI.FlutterForegroundTask.initCommunicationPort();
runApp(constExampleApp());
}
Write a TaskHandler and a callback to request starting a TaskHandler.
// The callback function should always be a top-level or static function.@pragma('vm:entry-point')
voidstartCallback() {
FlutterForegroundTask.setTaskHandler(MyTaskHandler());
}
classMyTaskHandlerextendsTaskHandler{
// Called when the task is started.@overrideFuture<void> onStart(DateTime timestamp, TaskStarter starter) async {
print('onStart(starter: ${starter.name})');
}
// Called based on the eventAction set in ForegroundTaskOptions.@overridevoidonRepeatEvent(DateTime timestamp) {
// Send data to main isolate.finalMap<String, dynamic> data = {
"timestampMillis": timestamp.millisecondsSinceEpoch,
};
FlutterForegroundTask.sendDataToMain(data);
}
// Called when the task is destroyed.@overrideFuture<void> onDestroy(DateTime timestamp, bool isTimeout) async {
print('onDestroy(isTimeout: $isTimeout)');
}
// Called when data is sent using `FlutterForegroundTask.sendDataToTask`.@overridevoidonReceiveData(Object data) {
print('onReceiveData: $data');
}
// Called when the notification button is pressed.@overridevoidonNotificationButtonPressed(String id) {
print('onNotificationButtonPressed: $id');
}
// Called when the notification itself is pressed.@overridevoidonNotificationPressed() {
print('onNotificationPressed');
}
// Called when the notification itself is dismissed.@overridevoidonNotificationDismissed() {
print('onNotificationDismissed');
}
}
Add a callback to receive data sent from the TaskHandler. If the screen or controller is disposed, be sure to call the removeTaskDataCallback function.
void_onReceiveTaskData(Object data) {
if (data isMap<String, dynamic>) {
finaldynamic timestampMillis = data["timestampMillis"];
if (timestampMillis != null) {
finalDateTime timestamp =
DateTime.fromMillisecondsSinceEpoch(timestampMillis, isUtc: true);
print('timestamp: ${timestamp.toString()}');
}
}
}
@overridevoidinitState() {
super.initState();
// Add a callback to receive data sent from the TaskHandler.FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
}
@overridevoiddispose() {
// Remove a callback to receive data sent from the TaskHandler.FlutterForegroundTask.removeTaskDataCallback(_onReceiveTaskData);
super.dispose();
}
Request permissions and initialize the service.
Future<void> _requestPermissions() async {
// Android 13+, you need to allow notification permission to display foreground service notification.//// iOS: If you need notification, ask for permission.finalNotificationPermission notificationPermission =
awaitFlutterForegroundTask.checkNotificationPermission();
if (notificationPermission != NotificationPermission.granted) {
awaitFlutterForegroundTask.requestNotificationPermission();
}
if (Platform.isAndroid) {
// Android 12+, there are restrictions on starting a foreground service.//// To restart the service on device reboot or unexpected problem, you need to allow below permission.if (!awaitFlutterForegroundTask.isIgnoringBatteryOptimizations) {
// This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.awaitFlutterForegroundTask.requestIgnoreBatteryOptimization();
}
// Use this utility only if you provide services that require long-term survival,// such as exact alarm service, healthcare service, or Bluetooth communication.//// This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission.// Using this permission may make app distribution difficult due to Google policy.if (!awaitFlutterForegroundTask.canScheduleExactAlarms) {
// When you call this function, will be gone to the settings page. // So you need to explain to the user why set it.awaitFlutterForegroundTask.openAlarmsAndRemindersSettings();
}
}
}
void_initService() {
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'foreground_service',
channelName: 'Foreground Service Notification',
channelDescription:
'This notification appears when the foreground service is running.',
onlyAlertOnce: true,
),
iosNotificationOptions: constIOSNotificationOptions(
showNotification: false,
playSound: false,
),
foregroundTaskOptions: ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(5000),
autoRunOnBoot: true,
autoRunOnMyPackageReplaced: true,
allowWakeLock: true,
allowWifiLock: true,
),
);
}
@overridevoidinitState() {
super.initState();
// Add a callback to receive data sent from the TaskHandler.FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData);
WidgetsBinding.instance.addPostFrameCallback((_) {
// Request permissions and initialize the service._requestPermissions();
_initService();
});
}
Use FlutterForegroundTask.startService to start the service. startService provides the following options:
serviceId: The unique ID that identifies the service.
notificationTitle: The title to display in the notification.
notificationText: The text to display in the notification.
notificationIcon: The icon to display in the notification. Go to this page to customize.
notificationButtons: The buttons to display in the notification. (can add 0~3 buttons)
notificationInitialRoute: Initial route to be used when the app is launched via a notification. Works the same as the launchApp utility.
callback: A top-level function that calls the setTaskHandler function.
Future<ServiceRequestResult> _startService() async {
if (awaitFlutterForegroundTask.isRunningService) {
returnFlutterForegroundTask.restartService();
} else {
returnFlutterForegroundTask.startService(
// You can manually specify the foregroundServiceType for the service// to be started, as shown in the comment below.// serviceTypes: [// ForegroundServiceTypes.dataSync,// ForegroundServiceTypes.remoteMessaging,// ],
serviceId: 256,
notificationTitle: 'Foreground Service is running',
notificationText: 'Tap to return to the app',
notificationIcon: null,
notificationButtons: [
constNotificationButton(id: 'btn_hello', text: 'hello'),
],
notificationInitialRoute: '/',
callback: startCallback,
);
}
}
[!NOTE]
iOS Platform, notificationButtons is not displayed directly in notification.
When the user slides down the notification, the button is displayed, so you need to guide the user on how to use it.
https://developer.apple.com/documentation/usernotifications/declaring-your-actionable-notification-types
Use FlutterForegroundTask.updateService to update the service. The options are the same as the start function.
// Main(UI)void_sendDataToTask() {
// Main(UI) -> TaskHandler//// The Map collection can only be sent in json format, such as Map<String, dynamic>.FlutterForegroundTask.sendDataToTask(Object);
}
// TaskHandler@overridevoidonReceiveData(Object data) {
print('onReceiveData: $data');
// You can cast it to any type you want using the Collection.cast<T> function.if (data isList<dynamic>) {
finalList<int> intList = data.cast<int>();
}
}
And there are some functions for storing and managing data that are only used in this plugin.