|
| 1 | +--- |
| 2 | +title: Task Status Tracking |
| 3 | +description: Understanding background task lifecycle and status notifications |
| 4 | +--- |
| 5 | + |
| 6 | +Workmanager provides detailed task status tracking through debug handlers, allowing you to monitor the complete lifecycle of your background tasks from scheduling to completion. |
| 7 | + |
| 8 | +## Task Status Overview |
| 9 | + |
| 10 | +Background tasks go through several status states during their lifecycle. The Workmanager plugin tracks these states and provides notifications through debug handlers. |
| 11 | + |
| 12 | +## Task Status States |
| 13 | + |
| 14 | +| Status | Description | When it occurs | Android | iOS | |
| 15 | +|--------|-------------|----------------|---------|-----| |
| 16 | +| **Scheduled** | Task has been scheduled with the system | When `registerOneOffTask()` or `registerPeriodicTask()` is called | ✅ | ✅ | |
| 17 | +| **Started** | Task execution has begun (first attempt) | When task starts running for the first time | ✅ | ✅ | |
| 18 | +| **Retrying** | Task is being retried after a previous attempt | When task starts running after `runAttemptCount > 0` | ✅ | ❌ | |
| 19 | +| **Rescheduled** | Task will be retried later | When Dart function returns `false` | ✅ | ❌ | |
| 20 | +| **Completed** | Task finished successfully | When Dart function returns `true` | ✅ | ✅ | |
| 21 | +| **Failed** | Task failed permanently | When Dart function throws an exception | ✅ | ✅ | |
| 22 | +| **Cancelled** | Task was cancelled before completion | When `cancelAll()` or `cancelByUniqueName()` is called | ✅ | ✅ | |
| 23 | + |
| 24 | +## Task Result Behavior |
| 25 | + |
| 26 | +The behavior of task status depends on what your Dart background function returns: |
| 27 | + |
| 28 | +### Dart Function Return Values |
| 29 | + |
| 30 | +<Tabs> |
| 31 | + <TabItem label="Android" value="android"> |
| 32 | + |
| 33 | +| Dart Return | Task Status | System Behavior | Debug Notification | |
| 34 | +|-------------|-------------|-----------------|-------------------| |
| 35 | +| `true` | **Completed** | Task succeeds, won't retry | ✅ Success | |
| 36 | +| `false` | **Rescheduled** | WorkManager schedules retry with backoff | 🔄 Rescheduled | |
| 37 | +| `Future.error()` | **Failed** | Task fails permanently, no retry | ❌ Failed + error | |
| 38 | +| Exception thrown | **Failed** | Task fails permanently, no retry | ❌ Failed + error | |
| 39 | + |
| 40 | + </TabItem> |
| 41 | + <TabItem label="iOS" value="ios"> |
| 42 | + |
| 43 | +| Dart Return | Task Status | System Behavior | Debug Notification | |
| 44 | +|-------------|-------------|-----------------|-------------------| |
| 45 | +| `true` | **Completed** | Task succeeds, won't retry | ✅ Success | |
| 46 | +| `false` | **Retrying** | App must manually reschedule | 🔄 Retrying | |
| 47 | +| `Future.error()` | **Failed** | Task fails, no automatic retry | ❌ Failed + error | |
| 48 | +| Exception thrown | **Failed** | Task fails, no automatic retry | ❌ Failed + error | |
| 49 | + |
| 50 | + </TabItem> |
| 51 | +</Tabs> |
| 52 | + |
| 53 | +## Advanced Android Features |
| 54 | + |
| 55 | +### Retry Detection |
| 56 | + |
| 57 | +On Android, Workmanager can distinguish between first attempts and retries using `runAttemptCount`: |
| 58 | + |
| 59 | +| Scenario | Start Status | End Status | Notification Example | |
| 60 | +|----------|--------------|------------|---------------------| |
| 61 | +| Fresh task, succeeds | **Started** | **Completed** | ▶️ Started → ✅ Success | |
| 62 | +| Fresh task, returns false | **Started** | **Rescheduled** | ▶️ Started → 🔄 Rescheduled | |
| 63 | +| Retry attempt, succeeds | **Retrying** | **Completed** | 🔄 Retrying → ✅ Success | |
| 64 | +| Retry attempt, fails | **Retrying** | **Failed** | 🔄 Retrying → ❌ Failed | |
| 65 | + |
| 66 | +### Backoff Policy |
| 67 | + |
| 68 | +When a task returns `false`, Android WorkManager uses exponential backoff by default: |
| 69 | +- 1st retry: ~30 seconds |
| 70 | +- 2nd retry: ~1 minute |
| 71 | +- 3rd retry: ~2 minutes |
| 72 | +- Maximum: ~5 hours |
| 73 | + |
| 74 | +## Debug Handler Integration |
| 75 | + |
| 76 | +Task status is exposed through debug handlers. Set up debug handlers to receive status notifications: |
| 77 | + |
| 78 | +### Notification Configuration |
| 79 | + |
| 80 | +The `NotificationDebugHandler` supports custom notification channels and grouping: |
| 81 | + |
| 82 | +**Android Options:** |
| 83 | +- `channelId`: Custom notification channel ID for organizing notifications (if custom, you must create the channel first) |
| 84 | +- `channelName`: Human-readable channel name shown in system settings (only used if using default channel) |
| 85 | +- `groupKey`: Groups related notifications together in the notification drawer |
| 86 | + |
| 87 | +**iOS Options:** |
| 88 | +- `categoryIdentifier`: Custom notification category for specialized handling |
| 89 | +- `threadIdentifier`: Groups notifications in the same conversation thread |
| 90 | + |
| 91 | +<Tabs> |
| 92 | + <TabItem label="Android" value="android"> |
| 93 | + |
| 94 | +```kotlin |
| 95 | +// NotificationDebugHandler - shows status as notifications |
| 96 | +WorkmanagerDebug.setCurrent(NotificationDebugHandler()) |
| 97 | + |
| 98 | +// Custom notification channel and grouping (you must create the channel first) |
| 99 | +val channelId = "MyAppDebugChannel" |
| 100 | +if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
| 101 | + val channel = NotificationChannel(channelId, "My App Debug", NotificationManager.IMPORTANCE_DEFAULT) |
| 102 | + val notificationManager = getSystemService(NotificationManager::class.java) |
| 103 | + notificationManager.createNotificationChannel(channel) |
| 104 | +} |
| 105 | +WorkmanagerDebug.setCurrent(NotificationDebugHandler( |
| 106 | + channelId = channelId, |
| 107 | + groupKey = "workmanager_debug_group" |
| 108 | +)) |
| 109 | + |
| 110 | +// LoggingDebugHandler - writes to system log |
| 111 | +WorkmanagerDebug.setCurrent(LoggingDebugHandler()) |
| 112 | + |
| 113 | +// Custom handler |
| 114 | +class CustomDebugHandler : WorkmanagerDebug() { |
| 115 | + override fun onTaskStatusUpdate( |
| 116 | + context: Context, |
| 117 | + taskInfo: TaskDebugInfo, |
| 118 | + status: TaskStatus, |
| 119 | + result: TaskResult? |
| 120 | + ) { |
| 121 | + when (status) { |
| 122 | + TaskStatus.SCHEDULED -> log("Task scheduled: ${taskInfo.taskName}") |
| 123 | + TaskStatus.STARTED -> log("Task started: ${taskInfo.taskName}") |
| 124 | + TaskStatus.RETRYING -> log("Task retrying (attempt ${taskInfo.runAttemptCount}): ${taskInfo.taskName}") |
| 125 | + TaskStatus.RESCHEDULED -> log("Task rescheduled: ${taskInfo.taskName}") |
| 126 | + TaskStatus.COMPLETED -> log("Task completed: ${taskInfo.taskName}") |
| 127 | + TaskStatus.FAILED -> log("Task failed: ${taskInfo.taskName}, error: ${result?.error}") |
| 128 | + TaskStatus.CANCELLED -> log("Task cancelled: ${taskInfo.taskName}") |
| 129 | + } |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | + </TabItem> |
| 135 | + <TabItem label="iOS" value="ios"> |
| 136 | + |
| 137 | +```swift |
| 138 | +// NotificationDebugHandler - shows status as notifications |
| 139 | +WorkmanagerDebug.setCurrent(NotificationDebugHandler()) |
| 140 | + |
| 141 | +// Custom notification category and thread grouping |
| 142 | +WorkmanagerDebug.setCurrent(NotificationDebugHandler( |
| 143 | + categoryIdentifier: "myAppDebugCategory", |
| 144 | + threadIdentifier: "workmanager_debug_thread" |
| 145 | +)) |
| 146 | + |
| 147 | +// LoggingDebugHandler - writes to system log |
| 148 | +WorkmanagerDebug.setCurrent(LoggingDebugHandler()) |
| 149 | + |
| 150 | +// Custom handler |
| 151 | +class CustomDebugHandler: WorkmanagerDebug { |
| 152 | + override func onTaskStatusUpdate(taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) { |
| 153 | + switch status { |
| 154 | + case .scheduled: |
| 155 | + print("Task scheduled: \(taskInfo.taskName)") |
| 156 | + case .started: |
| 157 | + print("Task started: \(taskInfo.taskName)") |
| 158 | + case .retrying: |
| 159 | + print("Task retrying: \(taskInfo.taskName)") |
| 160 | + case .rescheduled: |
| 161 | + print("Task rescheduled: \(taskInfo.taskName)") |
| 162 | + case .completed: |
| 163 | + print("Task completed: \(taskInfo.taskName)") |
| 164 | + case .failed: |
| 165 | + print("Task failed: \(taskInfo.taskName), error: \(result?.error ?? "unknown")") |
| 166 | + case .cancelled: |
| 167 | + print("Task cancelled: \(taskInfo.taskName)") |
| 168 | + } |
| 169 | + } |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | + </TabItem> |
| 174 | +</Tabs> |
| 175 | + |
| 176 | +## Notification Format |
| 177 | + |
| 178 | +The built-in `NotificationDebugHandler` shows concise, actionable notifications: |
| 179 | + |
| 180 | +### Notification Examples |
| 181 | + |
| 182 | +| Status | Title Format | Body | |
| 183 | +|--------|--------------|------| |
| 184 | +| Scheduled | 📅 Scheduled | taskName | |
| 185 | +| Started | ▶️ Started | taskName | |
| 186 | +| Retrying | 🔄 Retrying | taskName | |
| 187 | +| Rescheduled | 🔄 Rescheduled | taskName | |
| 188 | +| Success | ✅ Success | taskName | |
| 189 | +| Failed | ❌ Failed | taskName + error message | |
| 190 | +| Exception | ❌ Exception | taskName + exception details | |
| 191 | +| Cancelled | ⏹️ Cancelled | taskName | |
| 192 | + |
| 193 | +## Platform Differences |
| 194 | + |
| 195 | +### Android Advantages |
| 196 | +- **Retry detection**: Can distinguish first attempts from retries |
| 197 | +- **Automatic rescheduling**: WorkManager handles retry logic with backoff |
| 198 | +- **Rich debug info**: Access to `runAttemptCount` and system constraints |
| 199 | +- **Guaranteed execution**: Tasks will retry according to policy |
| 200 | + |
| 201 | +### iOS Limitations |
| 202 | +- **No retry detection**: Cannot distinguish first attempts from retries |
| 203 | +- **Manual rescheduling**: App must reschedule tasks on failure |
| 204 | +- **System controlled**: iOS decides when/if tasks actually run |
| 205 | +- **No guarantees**: Tasks may never execute depending on system state |
| 206 | + |
| 207 | +## Best Practices |
| 208 | + |
| 209 | +### Task Implementation |
| 210 | + |
| 211 | +```dart |
| 212 | +@pragma('vm:entry-point') |
| 213 | +void callbackDispatcher() { |
| 214 | + Workmanager().executeTask((task, inputData) async { |
| 215 | + try { |
| 216 | + // Your task logic here |
| 217 | + final result = await performWork(task, inputData); |
| 218 | + |
| 219 | + if (result.isSuccess) { |
| 220 | + return true; // ✅ Task succeeded |
| 221 | + } else { |
| 222 | + return false; // 🔄 Retry with backoff (Android) or manual reschedule (iOS) |
| 223 | + } |
| 224 | + } catch (e) { |
| 225 | + // 🔥 Permanent failure - will not retry |
| 226 | + throw Exception('Task failed: $e'); |
| 227 | + } |
| 228 | + }); |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### Error Handling Strategy |
| 233 | + |
| 234 | +| Error Type | Recommended Return | Result | |
| 235 | +|------------|-------------------|--------| |
| 236 | +| Network timeout | `return false` | Task will retry later | |
| 237 | +| Invalid data | `throw Exception()` | Task fails permanently | |
| 238 | +| Temporary server error | `return false` | Task will retry with backoff | |
| 239 | +| Authentication failure | `throw Exception()` | Task fails, needs user intervention | |
| 240 | + |
| 241 | +### Monitoring Task Health |
| 242 | + |
| 243 | +```dart |
| 244 | +// Track task execution in your debug handler |
| 245 | +class TaskHealthMonitor : WorkmanagerDebug() { |
| 246 | + override fun onTaskStatusUpdate(context: Context, taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) { |
| 247 | + when (status) { |
| 248 | + TaskStatus.COMPLETED -> recordSuccess(taskInfo.taskName) |
| 249 | + TaskStatus.FAILED -> recordFailure(taskInfo.taskName, result?.error) |
| 250 | + TaskStatus.RETRYING -> recordRetry(taskInfo.taskName) |
| 251 | + } |
| 252 | + } |
| 253 | +} |
| 254 | +``` |
| 255 | + |
| 256 | +## Troubleshooting |
| 257 | + |
| 258 | +### Common Issues |
| 259 | + |
| 260 | +**Tasks showing as "Rescheduled" but not running:** |
| 261 | +- Android: Check battery optimization and Doze mode settings |
| 262 | +- iOS: Verify Background App Refresh is enabled and app is used regularly |
| 263 | + |
| 264 | +**Tasks immediately failing:** |
| 265 | +- Check if task logic throws exceptions during initialization |
| 266 | +- Verify all dependencies are available in background isolate |
| 267 | +- Review error messages in Failed notifications |
| 268 | + |
| 269 | +**No status notifications appearing:** |
| 270 | +- Ensure debug handler is set before task execution |
| 271 | +- Check notification permissions (for NotificationDebugHandler) |
| 272 | +- Verify debug handler is called during task lifecycle |
| 273 | + |
| 274 | +For detailed debugging guidance, see the [Debugging Guide](debugging). |
| 275 | + |
| 276 | +## Migration from isInDebugMode |
| 277 | + |
| 278 | +If you were using the deprecated `isInDebugMode` parameter: |
| 279 | + |
| 280 | +```dart |
| 281 | +// ❌ Old approach (deprecated) |
| 282 | +await Workmanager().initialize( |
| 283 | + callbackDispatcher, |
| 284 | + isInDebugMode: true, // Deprecated |
| 285 | +); |
| 286 | +
|
| 287 | +// ✅ New approach |
| 288 | +await Workmanager().initialize(callbackDispatcher); |
| 289 | +
|
| 290 | +// Set up platform-specific debug handler |
| 291 | +// Android: WorkmanagerDebug.setCurrent(NotificationDebugHandler()) |
| 292 | +// iOS: WorkmanagerDebug.setCurrent(LoggingDebugHandler()) |
| 293 | +``` |
| 294 | + |
| 295 | +The new system provides much more detailed and customizable debugging information than the simple boolean flag. |
0 commit comments