Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions docs/debugging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,61 @@ class AppDelegate: FlutterAppDelegate {
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)

WorkmanagerDebug.setCurrent(LoggingDebugHandler())

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
```

### Notification Debug Handler

Shows debug information as notifications:
Shows debug information as notifications (helpful for seeing background task execution):

```swift
WorkmanagerDebug.setCurrent(NotificationDebugHandler())
// In your AppDelegate.swift
import workmanager_apple

@main
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)

// Set notification delegate first
UNUserNotificationCenter.current().delegate = self

// Request notification permission
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted {
print("Notification permission granted")
}
}

// Enable notification debug handler
WorkmanagerDebug.setCurrent(NotificationDebugHandler())

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

// REQUIRED: Override to show notifications when app is in foreground
override func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler(.alert) // Show notification banner even if app is in foreground
}
}
```

<Warning>
**Important:** The `userNotificationCenter` override is required to display debug notifications when your app is in the foreground. Without it, you'll only see notifications when the app is in the background.
</Warning>

## Custom Debug Handlers

Create your own debug handler for custom logging needs:
Expand Down Expand Up @@ -361,4 +402,4 @@ Future<bool> isTaskHealthy(String taskName, Duration maxAge) async {
- [ ] Error handling with try-catch blocks
- [ ] iOS 30-second execution limit respected

Remember: Background task execution is controlled by the operating system and is never guaranteed. Always design your app to work gracefully when background tasks don't run as expected.
Remember: Background task execution is controlled by the operating system and is never guaranteed. Always design your app to work gracefully when background tasks don't run as expected.
19 changes: 14 additions & 5 deletions docs/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ WorkmanagerPlugin.registerBGProcessingTask(
)
```

<Warning>
**iOS Task Identifier Matching:** The task name in your Dart code must **exactly match** the identifier in Info.plist and AppDelegate. Using short names like `"data_sync"` in Dart while having `com.yourapp.processing_task` in native code will cause `BGTaskSchedulerErrorDomain Code 3` errors.
</Warning>

<Info>
**Why BGTaskScheduler registration is needed:** iOS requires explicit registration of background task identifiers for security and system resource management. The task identifier in Info.plist tells iOS which background tasks your app can schedule, while the AppDelegate registration connects the identifier to the actual task handler. Background Fetch (Option A) doesn't require this since it uses the simpler, system-managed approach.
</Info>
Expand Down Expand Up @@ -108,6 +112,10 @@ WorkmanagerPlugin.registerPeriodicTask(
)
```

<Warning>
**iOS Task Identifier Matching:** The task name in your Dart code must **exactly match** the identifier in Info.plist and AppDelegate. Using short names like `"cleanup"` in Dart while having `com.yourapp.periodic_task` in native code will cause `BGTaskSchedulerErrorDomain Code 3` errors.
</Warning>

<Success>
**Which option to choose?**
- **Option A (Background Fetch)** for non-critical updates that can happen once daily (data sync, content refresh)
Expand All @@ -124,10 +132,10 @@ WorkmanagerPlugin.registerPeriodicTask(
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
switch (task) {
case "data_sync":
case "com.yourapp.processing_task": // Must match Info.plist and AppDelegate
await syncDataWithServer();
break;
case "cleanup":
case "com.yourapp.periodic_task": // Must match Info.plist and AppDelegate
await cleanupOldFiles();
break;
case Workmanager.iOSBackgroundTask:
Expand Down Expand Up @@ -166,14 +174,14 @@ void main() {
// Schedule a one-time task
Workmanager().registerOneOffTask(
"sync-task",
"data_sync",
"com.yourapp.processing_task", // Must match Info.plist and AppDelegate
initialDelay: Duration(seconds: 10),
);

// Schedule a periodic task
Workmanager().registerPeriodicTask(
"cleanup-task",
"cleanup",
"com.yourapp.periodic_task", // Must match Info.plist and AppDelegate
frequency: Duration(hours: 24),
);
```
Expand All @@ -191,6 +199,7 @@ Your background tasks can return:

- **Callback Dispatcher**: Must be a top-level function (not inside a class)
- **Separate Isolate**: Background tasks run in isolation - initialize dependencies inside the task
- **iOS Task Identifiers**: When using BGTaskScheduler (Options B & C), task names in Dart must exactly match identifiers in Info.plist and AppDelegate
- **Platform Differences**:
- Android: Reliable background execution, 15-minute minimum frequency
- iOS: 30-second limit, execution depends on user patterns and device state
Expand All @@ -200,4 +209,4 @@ Your background tasks can return:

- **[Task Customization](customization)** - Advanced configuration with constraints, input data, and management
- **[Debugging Guide](debugging)** - Learn how to debug and troubleshoot background tasks
- **[Example App](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** - Complete working demo
- **[Example App](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** - Complete working demo
24 changes: 24 additions & 0 deletions workmanager/lib/src/workmanager_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,30 @@ class Workmanager {
);
}

/// Schedule a health research background task, currently only available on iOS 15+.
///
/// Health research tasks use [BGHealthResearchTask] and are designed for
/// apps that need to process HealthKit data in the background. The system
/// grants these tasks more runtime than standard background tasks.
///
/// Requires iOS 15.0 or later. Will throw [UnsupportedError] on Android.
///
/// For iOS see Apple docs:
/// [iOS 15+ BGHealthResearchTask](https://developer.apple.com/documentation/backgroundtasks/bghealthresearchtask/)
Future<void> registerHealthResearchTask(
String uniqueName,
String taskName, {
Duration? initialDelay,
Map<String, dynamic>? inputData,
}) async {
return _platform.registerHealthResearchTask(
uniqueName,
taskName,
initialDelay: initialDelay,
inputData: inputData,
);
}

/// Cancels task by [uniqueName]
Future<void> cancelByUniqueName(String uniqueName) async =>
_platform.cancelByUniqueName(uniqueName);
Expand Down
43 changes: 43 additions & 0 deletions workmanager/test/workmanager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,48 @@ void main() {
verify(GetIt.I<Workmanager>().initialize(testCallBackDispatcher));
verify(GetIt.I<Workmanager>().cancelByUniqueName(testTaskName));
});

test("registerHealthResearchTask - It calls methods on the mocked class",
() {
when(GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
)).thenAnswer((_) => Future.value());

GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
);

verify(GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
));
});

test(
"registerHealthResearchTask with optional params - It calls methods on the mocked class",
() {
when(GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
initialDelay: const Duration(minutes: 10),
inputData: {'dataType': 'heartRate'},
)).thenAnswer((_) => Future.value());

GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
initialDelay: const Duration(minutes: 10),
inputData: {'dataType': 'heartRate'},
);

verify(GetIt.I<Workmanager>().registerHealthResearchTask(
'com.example.health-task',
'healthTask',
initialDelay: const Duration(minutes: 10),
inputData: {'dataType': 'heartRate'},
));
});
});
}
24 changes: 24 additions & 0 deletions workmanager/test/workmanager_test.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import 'package:workmanager_platform_interface/workmanager_platform_interface.da
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member

/// A class which mocks [Workmanager].
///
Expand Down Expand Up @@ -165,6 +166,29 @@ class MockWorkmanager extends _i1.Mock implements _i2.Workmanager {
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);

@override
_i3.Future<void> registerHealthResearchTask(
String? uniqueName,
String? taskName, {
Duration? initialDelay,
Map<String, dynamic>? inputData,
}) =>
(super.noSuchMethod(
Invocation.method(
#registerHealthResearchTask,
[
uniqueName,
taskName,
],
{
#initialDelay: initialDelay,
#inputData: inputData,
},
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);

@override
_i3.Future<void> cancelByUniqueName(String? uniqueName) =>
(super.noSuchMethod(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.fluttercommunity.workmanager

import dev.fluttercommunity.workmanager.pigeon.HealthResearchTaskRequest
import dev.fluttercommunity.workmanager.pigeon.InitializeRequest
import dev.fluttercommunity.workmanager.pigeon.OneOffTaskRequest
import dev.fluttercommunity.workmanager.pigeon.PeriodicTaskRequest
Expand Down Expand Up @@ -103,6 +104,14 @@ class WorkmanagerPlugin :
callback(Result.failure(UnsupportedOperationException("Processing tasks are not supported on Android")))
}

override fun registerHealthResearchTask(
request: HealthResearchTaskRequest,
callback: (Result<Unit>) -> Unit,
) {
// Health research tasks are iOS 15+ specific
callback(Result.failure(UnsupportedOperationException("Health research tasks are not supported on Android")))
}

override fun cancelByUniqueName(
uniqueName: String,
callback: (Result<Unit>) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// // Copyright 2024 The Flutter Workmanager Authors. All rights reserved.
// // Use of this source code is governed by a MIT-style license that can be
// // found in the LICENSE file.
// Autogenerated from Pigeon (v26.0.1), do not edit directly.
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down Expand Up @@ -510,6 +510,43 @@ data class ProcessingTaskRequest (

override fun hashCode(): Int = toList().hashCode()
}

/** Generated class from Pigeon that represents data sent in messages. */
data class HealthResearchTaskRequest (
val uniqueName: String,
val taskName: String,
val inputData: Map<String?, Any?>? = null,
val initialDelaySeconds: Long? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): HealthResearchTaskRequest {
val uniqueName = pigeonVar_list[0] as String
val taskName = pigeonVar_list[1] as String
val inputData = pigeonVar_list[2] as Map<String?, Any?>?
val initialDelaySeconds = pigeonVar_list[3] as Long?
return HealthResearchTaskRequest(uniqueName, taskName, inputData, initialDelaySeconds)
}
}
fun toList(): List<Any?> {
return listOf(
uniqueName,
taskName,
inputData,
initialDelaySeconds,
)
}
override fun equals(other: Any?): Boolean {
if (other !is HealthResearchTaskRequest) {
return false
}
if (this === other) {
return true
}
return WorkmanagerApiPigeonUtils.deepEquals(toList(), other.toList()) }

override fun hashCode(): Int = toList().hashCode()
}
private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
Expand Down Expand Up @@ -573,6 +610,11 @@ private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
ProcessingTaskRequest.fromList(it)
}
}
141.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HealthResearchTaskRequest.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
Expand Down Expand Up @@ -626,6 +668,10 @@ private open class WorkmanagerApiPigeonCodec : StandardMessageCodec() {
stream.write(140)
writeValue(stream, value.toList())
}
is HealthResearchTaskRequest -> {
stream.write(141)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
Expand All @@ -638,6 +684,7 @@ interface WorkmanagerHostApi {
fun registerOneOffTask(request: OneOffTaskRequest, callback: (Result<Unit>) -> Unit)
fun registerPeriodicTask(request: PeriodicTaskRequest, callback: (Result<Unit>) -> Unit)
fun registerProcessingTask(request: ProcessingTaskRequest, callback: (Result<Unit>) -> Unit)
fun registerHealthResearchTask(request: HealthResearchTaskRequest, callback: (Result<Unit>) -> Unit)
fun cancelByUniqueName(uniqueName: String, callback: (Result<Unit>) -> Unit)
fun cancelByTag(tag: String, callback: (Result<Unit>) -> Unit)
fun cancelAll(callback: (Result<Unit>) -> Unit)
Expand Down Expand Up @@ -729,6 +776,25 @@ interface WorkmanagerHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.registerHealthResearchTask$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val requestArg = args[0] as HealthResearchTaskRequest
api.registerHealthResearchTask(requestArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(WorkmanagerApiPigeonUtils.wrapError(error))
} else {
reply.reply(WorkmanagerApiPigeonUtils.wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.workmanager_platform_interface.WorkmanagerHostApi.cancelByUniqueName$separatedMessageChannelSuffix", codec)
if (api != null) {
Expand Down
Loading
Loading