Push Notifications & Background Services

Constraints, Retry, and Task Lifecycle with Workmanager

15 min Lesson 9 of 11

Constraints, Retry, and Task Lifecycle with Workmanager

Workmanager lets you schedule background tasks that run under specific device conditions. By attaching constraints to a task, you ensure it only executes when the device meets your requirements—such as having an active network connection or being plugged in. This lesson covers how to define constraints, configure back-off policies for automatic retries, cancel tasks by name or tag, and interpret the result values your task returns.

Defining Execution Constraints

A Constraints object is passed to registerOneOffTask or registerPeriodicTask via the named parameter constraints. The most commonly used constraint properties are:

  • networkTypeNetworkType.connected, NetworkType.metered, NetworkType.unmetered, or NetworkType.not_required. Use unmetered for large uploads/downloads so they only run on Wi-Fi.
  • requiresBatteryNotLowtrue defers execution when the battery drops below a system threshold (roughly 20%).
  • requiresChargingtrue ensures the task runs only while the device is connected to a charger.
  • requiresDeviceIdletrue delays the task until the device is idle (Android only; ignored on iOS).
  • requiresStorageNotLowtrue skips execution when available storage is critically low.

Registering a Task with Constraints

import 'package:workmanager/workmanager.dart';

Future<void> scheduleConstrainedSync() async {
  await Workmanager().registerOneOffTask(
    'syncUserData',               // unique task name
    'syncUserDataTask',           // task identifier (passed to callbackDispatcher)
    initialDelay: const Duration(minutes: 5),
    constraints: Constraints(
      networkType: NetworkType.unmetered,   // Wi-Fi only
      requiresBatteryNotLow: true,          // skip if battery is low
      requiresCharging: false,              // charger not required
    ),
  );
}
Note: On iOS, Workmanager uses BGTaskScheduler under the hood. Not all Android constraints have iOS equivalents. networkType and requiresBatteryNotLow map reasonably well, but requiresDeviceIdle and requiresStorageNotLow are silently ignored on iOS. Always test on both platforms.

Back-off Policies and Retry Behaviour

When your callbackDispatcher returns Future.value(false), Workmanager marks the task as failed and schedules a retry according to the configured back-off policy. Two policies are available:

  • BackoffPolicy.linear — retry delay grows linearly: initialDelay × attempt.
  • BackoffPolicy.exponential — retry delay doubles each attempt (capped at roughly 5 hours on Android). This is the default and is preferred for network-dependent tasks to avoid thundering-herd problems.

Set the initial back-off delay with backoffPolicyDelay and choose the policy with backoffPolicy:

Configuring a Back-off Policy

Future<void> scheduleWithRetry() async {
  await Workmanager().registerOneOffTask(
    'uploadReport',
    'uploadReportTask',
    constraints: Constraints(networkType: NetworkType.connected),
    backoffPolicy: BackoffPolicy.exponential,
    backoffPolicyDelay: const Duration(minutes: 2),
    // First retry after ~2 min, then ~4 min, ~8 min, etc.
  );
}

// In callbackDispatcher:
// return Future.value(true);   // success — task is removed from queue
// return Future.value(false);  // failure — Workmanager will retry
// throw Exception('fatal');    // also treated as failure; will retry
Tip: Always return Future.value(true) for tasks that completed successfully, even if the work yielded no data. Returning false unnecessarily wastes battery by triggering retries. Only return false when the operation genuinely failed and retrying later is worthwhile (e.g., a transient network error).

Task Result Values

The return value from your top-level callback function drives the task lifecycle:

  • Future.value(true)Success. The task is considered done and will not be retried (for one-off tasks) or will be rescheduled for the next period (for periodic tasks).
  • Future.value(false)Failure. The task will be retried according to the back-off policy, up to the system-defined retry limit.
  • An unhandled exception — treated the same as false; retry will occur. Wrap critical code in try/catch so you can log errors before returning false.

Cancelling Tasks

Workmanager provides three cancellation methods. Use whichever matches how the task was originally registered:

  • Workmanager().cancelByUniqueName('uniqueName') — cancels a specific one-off or periodic task by its unique name string.
  • Workmanager().cancelByTag('tagName') — cancels all tasks that were registered with a matching tag parameter. Useful for bulk cancellation (e.g., cancelling all tasks for a logged-out user).
  • Workmanager().cancelAll() — cancels every pending Workmanager task in the app. Use carefully; this cannot be undone.

Cancelling Tasks by Name and Tag

// Register tasks with a shared tag
Future<void> registerUserTasks(String userId) async {
  const tag = 'user_tasks';

  await Workmanager().registerPeriodicTask(
    'sync_$userId',
    'syncTask',
    tag: tag,
    frequency: const Duration(hours: 1),
    constraints: Constraints(networkType: NetworkType.connected),
  );

  await Workmanager().registerOneOffTask(
    'avatar_$userId',
    'avatarUploadTask',
    tag: tag,
  );
}

// On logout: cancel every task belonging to this user
Future<void> cancelUserTasks() async {
  await Workmanager().cancelByTag('user_tasks');
}

// Or cancel a single task by its unique name
Future<void> cancelSyncOnly(String userId) async {
  await Workmanager().cancelByUniqueName('sync_$userId');
}
Warning: cancelAll() also cancels any periodic tasks you still need, such as a scheduled cache-cleanup or log-upload task. Prefer cancelByTag with meaningful tag strings to avoid accidentally stopping unrelated background work.

Full Task Lifecycle Summary

Understanding the full lifecycle prevents common bugs:

  • Registered — The task enters the OS work queue. It may not start immediately if constraints are unmet.
  • Running — Constraints are satisfied; the callback dispatcher executes your task function.
  • Succeeded (true) — Task is dequeued. Periodic tasks are rescheduled for the next interval.
  • Failed (false or exception) — Task is retried after the back-off delay, up to the retry limit.
  • Cancelled — Task is removed from the queue; will not run or retry.
Key Takeaway: Combine constraints with an appropriate back-off policy to build robust, battery-friendly background tasks. Always return true on success and false on retriable failure. Tag your tasks so you can cancel related groups cleanly. Understanding these lifecycle transitions will save you hours of debugging unexpected task behaviour in production.