Skip to content

Commit 248802a

Browse files
enedclaude
andcommitted
feat: enhance debug system with task status tracking and configurable notifications
- Add TaskStatus.SCHEDULED notifications when tasks are registered - Add TaskStatus.RESCHEDULED status to distinguish from RETRYING - Fix notification flow: Started → Rescheduled → Retrying → Success - Add configurable notification channels and grouping for debug handlers - Update notification icons to cleaner symbols (▶️ ✅ ❌ 🔄 ⏹️ 📅) - Add comprehensive task status documentation with platform differences - Fix Android retry detection using runAttemptCount - Remove duplicate exception notifications for normal task failures - Update example apps to demonstrate new debug configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 44be9a8 commit 248802a

File tree

19 files changed

+621
-116
lines changed

19 files changed

+621
-116
lines changed

docs.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
"title": "Task Customization",
3030
"href": "/customization"
3131
},
32+
{
33+
"title": "Task Status Tracking",
34+
"href": "/task-status"
35+
},
3236
{
3337
"title": "Debugging",
3438
"href": "/debugging"

docs/debugging.mdx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,8 @@ Create your own debug handler for custom logging needs:
9393
```kotlin
9494
class CustomDebugHandler : WorkmanagerDebug() {
9595
override fun onTaskStatusUpdate(context: Context, taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) {
96-
when (status) {
97-
TaskStatus.STARTED -> // Task started logic
98-
TaskStatus.COMPLETED -> // Task completed logic
99-
// Handle other statuses
100-
}
96+
// Custom status handling logic
97+
// See Task Status documentation for detailed status information
10198
}
10299

103100
override fun onExceptionEncountered(context: Context, taskInfo: TaskDebugInfo?, exception: Throwable) {
@@ -114,13 +111,8 @@ WorkmanagerDebug.setCurrent(CustomDebugHandler())
114111
```swift
115112
class CustomDebugHandler: WorkmanagerDebug {
116113
override func onTaskStatusUpdate(taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) {
117-
switch status {
118-
case .started:
119-
// Task started logic
120-
case .completed:
121-
// Task completed logic
122-
// Handle other statuses
123-
}
114+
// Custom status handling logic
115+
// See Task Status documentation for detailed status information
124116
}
125117

126118
override func onExceptionEncountered(taskInfo: TaskDebugInfo?, exception: Error) {
@@ -134,6 +126,8 @@ WorkmanagerDebug.setCurrent(CustomDebugHandler())
134126
</TabItem>
135127
</Tabs>
136128

129+
For detailed information about task statuses, lifecycle, and notification formats, see the [Task Status Tracking](task-status) guide.
130+
137131
## Android Debugging
138132

139133
### Job Scheduler Inspection

docs/task-status.mdx

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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.

example/android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
22

3+
<!-- Required for notification debug handler -->
4+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
5+
36
<application
7+
android:name="dev.fluttercommunity.workmanager_example.ExampleApplication"
48
android:icon="@mipmap/ic_launcher"
59
android:label="workmanager_example">
610
<activity
7-
android:name="io.flutter.embedding.android.FlutterActivity"
11+
android:name="dev.fluttercommunity.workmanager_example.MainActivity"
812
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
913
android:hardwareAccelerated="true"
1014
android:launchMode="singleTop"

0 commit comments

Comments
 (0)