Background tasks can be tricky to debug since they run when your app is closed. Here's how to effectively debug and troubleshoot them on both platforms.
The Workmanager plugin uses a hook-based debug system that allows you to customize how debug information is handled.
Initialize Workmanager without any debug parameters:
await Workmanager().initialize(callbackDispatcher);Then set up platform-specific debug handlers as needed.
Shows debug information in Android's Log system (visible in adb logcat):
// In your Application class
import dev.fluttercommunity.workmanager.WorkmanagerDebug
import dev.fluttercommunity.workmanager.LoggingDebugHandler
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
}
}Shows debug information as notifications (requires notification permissions):
import dev.fluttercommunity.workmanager.NotificationDebugHandler
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
WorkmanagerDebug.setCurrent(NotificationDebugHandler())
}
}Shows debug information in iOS's unified logging system:
// In your AppDelegate.swift
import workmanager_apple
@main
class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}Shows debug information as notifications:
WorkmanagerDebug.setCurrent(NotificationDebugHandler())Create your own debug handler for custom logging needs:
class CustomDebugHandler : WorkmanagerDebug() {
override fun onTaskStatusUpdate(context: Context, taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) {
// Custom status handling logic
// See Task Status documentation for detailed status information
}
override fun onExceptionEncountered(context: Context, taskInfo: TaskDebugInfo?, exception: Throwable) {
// Handle exceptions
}
}
WorkmanagerDebug.setCurrent(CustomDebugHandler())For detailed information about task statuses, lifecycle, and notification formats, see the Task Status Tracking guide.
Use ADB to inspect Android's job scheduler:
# View scheduled jobs
adb shell dumpsys jobscheduler | grep yourapp
# View detailed job info
adb shell dumpsys jobscheduler yourapp
# Force run job (debug only)
adb shell cmd jobscheduler run -f yourapp JOB_ID# Monitor WorkManager logs
adb logcat | grep WorkManager
# Monitor app background execution
adb logcat | grep "yourapp"Tasks not running:
- Check battery optimization settings
- Verify app is not in "App Standby" mode
- Ensure device isn't in Doze mode
- Check if constraints are too restrictive
Tasks running too often:
- Android enforces minimum 15-minute intervals for periodic tasks
- Use appropriate constraints to limit execution
# Check if your app is whitelisted from battery optimization
adb shell dumpsys deviceidle whitelist
# Check battery optimization status
adb shell settings get global battery_saver_constants
# Force device into idle mode (testing)
adb shell dumpsys deviceidle force-idleiOS background tasks have limited execution time. Add detailed logging:
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
print('[iOS BG] Task started: $task at ${DateTime.now()}');
try {
// Your task logic
final result = await performTask();
print('[iOS BG] Task completed successfully');
return true;
} catch (e) {
print('[iOS BG] Task failed: $e');
return false;
}
});
}For Background Fetch tasks: Use Debug → Perform Fetch from the Xcode run menu while your app is running.
For BGTaskScheduler tasks (processing/periodic): Use Xcode's console to trigger tasks manually:
// Trigger specific BGTaskScheduler task
e -l objc -- (void)[[BGTaskScheduler sharedScheduler]
_simulateLaunchForTaskWithIdentifier:@"com.yourapp.task.identifier"]Check what tasks iOS has scheduled:
// iOS 13+ only
if (Platform.isIOS) {
final tasks = await Workmanager().printScheduledTasks();
print('Scheduled tasks: $tasks');
}This prints output like:
[BGTaskScheduler] Task Identifier: your.task.id earliestBeginDate: 2023.10.10 PM 11:10:12
[BGTaskScheduler] There are no scheduled tasksBackground App Refresh disabled:
- Check iOS Settings → General → Background App Refresh
- See Apple's Background App Refresh Guide
Tasks never run:
- App hasn't been used recently (iOS learning algorithm) - iOS Power Management
- Task identifiers don't match between Info.plist and AppDelegate - check iOS setup
- Missing BGTaskSchedulerPermittedIdentifiers in Info.plist - review iOS configuration
Tasks stop working:
- iOS battery optimization kicked in - WWDC 2020: Background execution demystified
- App removed from recent apps too often
- User disabled background refresh - check Background Fetch setup
- Task taking longer than 30 seconds - BGTaskScheduler Documentation
Tasks run but don't complete:
- Hitting 30-second execution limit - Background Tasks Best Practices
- Network requests timing out
- Heavy processing blocking the thread - WWDC 2019: Advances in App Background Execution
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
final startTime = DateTime.now();
print('🚀 Task started: $task');
print('📊 Input data: $inputData');
try {
// Your task implementation
final result = await performTask(task, inputData);
final duration = DateTime.now().difference(startTime);
print('✅ Task completed in ${duration.inSeconds}s');
return result;
} catch (e, stackTrace) {
final duration = DateTime.now().difference(startTime);
print('❌ Task failed after ${duration.inSeconds}s: $e');
print('📋 Stack trace: $stackTrace');
return false; // Retry
}
});
}Create a way to test your background logic from the UI:
// Add a debug button to test task logic
ElevatedButton(
onPressed: () async {
// Test the same logic that runs in background
final result = await performTask('test_task', {'debug': true});
print('Test result: $result');
},
child: Text('Test Task Logic'),
)Track when tasks last ran successfully:
Future<void> saveTaskExecutionTime(String taskName) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('${taskName}_last_run', DateTime.now().toIso8601String());
}
Future<bool> isTaskHealthy(String taskName, Duration maxAge) async {
final prefs = await SharedPreferences.getInstance();
final lastRunString = prefs.getString('${taskName}_last_run');
if (lastRunString == null) return false;
final lastRun = DateTime.parse(lastRunString);
final age = DateTime.now().difference(lastRun);
return age < maxAge;
}- Enable debug mode and install app
- Schedule task and verify it appears in job scheduler
- Put device to sleep and wait for execution
- Check debug notifications to confirm execution
- Use ADB commands to force execution if needed
- Test on physical device (simulator doesn't support background tasks)
- Enable Background App Refresh in iOS Settings
- Use Xcode debugger to trigger tasks immediately
- Monitor Xcode console for logging output
- Check iOS Settings > Battery for background activity
Quick Checks:
- Workmanager initialized in main()
- Task names are unique
- Platform setup completed (iOS setup guide)
- Debug handler configured (see Debug Handlers)
Performance & Reliability:
- Task logic optimized for background execution
- Dependencies initialized in background isolate
- 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.