Flutter Workmanager Periodic Sync: A Practical Guide for Production
Implement reliable periodic background sync in Flutter using workmanager: setup, code samples, iOS/Android nuances, constraints, retries, and best practices.
Image used for representation purposes only.
Overview
Periodic background sync keeps user data fresh without making them wait. In Flutter, the workmanager package wraps Android’s WorkManager (and integrates with iOS background tasks) so you can schedule reliable, battery‑friendly work—even when your app isn’t open. This article explains when to use periodic sync, how workmanager works under the hood, and the exact steps and code you need to ship a robust background sync for production.
When to use periodic sync
Use periodic sync when your app should:
- Refresh cached content (news, messages, catalog updates) at intervals.
- Push local changes from an offline‑first store (e.g., SQLite/Hive) to your backend.
- Pull lightweight deltas regularly so foreground screens load instantly.
Avoid periodic sync when you need exact timing (e.g., alarms) or long‑running jobs. Mobile OSes optimize for battery life—expect inexact timing and backoff under Doze/Low Power modes.
How workmanager works
- Android: WorkManager batches and defers work based on constraints (network, charging, battery) and system state. Periodic work has a minimum interval (commonly 15 minutes). It survives process death and device reboot.
- iOS: Background execution is opportunistic. The OS decides when to run tasks based on device usage, power, and your declared capabilities. Intervals are not guaranteed; think “eventually and repeatedly,” not “every X minutes on the dot.”
In Flutter, your background logic runs in a separate Dart isolate. The entry point must be a top‑level function and marked to avoid tree‑shaking.
Project setup
- Add dependency in pubspec.yaml (check pub.dev for the latest version):
dependencies:
flutter:
sdk: flutter
workmanager: # use the latest version from pub.dev
- Import and initialize in main.dart before runApp:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
// 1) Decide what to do based on task name
// 2) Perform quick, resilient I/O with timeouts
// 3) Write results to local storage (e.g., SQLite/Hive)
try {
final endpoint = inputData?["endpoint"] ?? "/sync";
final full = inputData?["full"] == true;
// TODO: call your repository and network layer
// await Repository().sync(full: full, endpoint: endpoint);
return Future.value(true); // success
} catch (e) {
// Throwing/returning false tells the scheduler this attempt failed;
// it may retry with backoff based on your policy.
return Future.value(false);
}
});
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Workmanager().initialize(
callbackDispatcher,
isInDebugMode: kDebugMode, // speeds up scheduling in debug builds
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(home: Scaffold());
}
Android configuration
- No manual manifest receivers are typically needed—WorkManager includes a default initializer.
- Ensure your app’s MainActivity has android:exported set (Android 12+ requirement).
- Consider network and battery constraints; WorkManager respects Doze and App Standby.
iOS configuration
Because iOS runs background work opportunistically, configuration matters.
- In Xcode, enable Capabilities → Background Modes and check:
- Background fetch
- Background processing
- Add identifiers and background modes to ios/Runner/Info.plist. Use the same identifier you pass as your task name.
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.example.app.sync</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
<string>fetch</string>
</array>
Notes:
- iOS determines when to run tasks; intervals are not guaranteed.
- Keep work short and network‑efficient. If the OS believes your task is wasteful, it will deprioritize future runs.
Registering a periodic sync
Call this once after login/onboarding or when the user enables “Background sync.”
import 'package:workmanager/workmanager.dart';
Future<void> schedulePeriodicSync() async {
// Unique name lets you replace/update this job later.
const uniqueName = 'syncTask';
const taskName = 'com.example.app.sync'; // also used on iOS
await Workmanager().registerPeriodicTask(
uniqueName,
taskName,
frequency: const Duration(hours: 1), // Android: >= 15 min
initialDelay: const Duration(minutes: 15), // start window
constraints: Constraints(
networkType: NetworkType.connected,
requiresBatteryNotLow: true,
),
backoffPolicy: BackoffPolicy.exponential,
backoffPolicyDelay: const Duration(minutes: 10),
inputData: {
'endpoint': '/sync',
'full': false,
},
existingWorkPolicy: ExistingWorkPolicy.keep, // or .replace
);
}
Tips:
- Choose an interval that balances freshness and battery (e.g., 1–3 hours for content apps; 12–24 hours for heavy syncs).
- Prefer ExistingWorkPolicy.keep to avoid resetting backoff windows unnecessarily.
“Daily at 3 AM” pattern
Exact timing is not guaranteed, but you can approximate by setting an initialDelay to “next 3 AM” and then a 24h period.
Duration _delayUntilNext3am() {
final now = DateTime.now();
var next = DateTime(now.year, now.month, now.day, 3);
if (!next.isAfter(now)) {
next = next.add(const Duration(days: 1));
}
return next.difference(now);
}
Future<void> scheduleDailySync() async {
await Workmanager().registerPeriodicTask(
'dailySync',
'com.example.app.daily',
frequency: const Duration(days: 1),
initialDelay: _delayUntilNext3am(),
constraints: const Constraints(networkType: NetworkType.connected),
);
}
Expect jitter of tens of minutes or more, especially on iOS and under battery constraints.
One‑off “sync now” button
Complement periodic sync with a manual trigger for power users:
Future<void> syncNow() async {
await Workmanager().registerOneOffTask(
'syncNow',
'com.example.app.sync',
constraints: const Constraints(networkType: NetworkType.connected),
backoffPolicy: BackoffPolicy.exponential,
inputData: {'endpoint': '/sync', 'full': true},
existingWorkPolicy: ExistingWorkPolicy.replace,
);
}
Designing the sync itself
Structure your background logic like a tiny offline‑first pipeline:
- Read pending local changes and POST them first (upload phase).
- Pull server deltas since lastSyncToken (download phase).
- Merge into local store transactionally.
- Update metadata (lastSyncAt, lastSyncToken).
Suggested repository facade used both in foreground and background isolates:
class SyncRepository {
final HttpClient client;
final LocalStore store;
Future<void> sync({required bool full, String endpoint = '/sync'}) async {
// Timeouts keep work bounded; retries are handled by WorkManager backoff.
await _uploadPendingChanges();
await _downloadDeltas(full: full, endpoint: endpoint);
await store.setLastSync(DateTime.now());
}
}
Operational tips:
- Bound each run to a few minutes. Large backlogs? Process in chunks and let the scheduler retry.
- Use ETags/If‑Modified‑Since or cursors to avoid full downloads.
- Be idempotent: re‑running must not corrupt state.
- Log minimally; noisy logs hurt battery.
Handling network, retries, and backoff
- Use Constraints(networkType: NetworkType.connected) to avoid futile attempts.
- Return false on transient failures; the scheduler will retry with exponential backoff.
- Return true on “permanent” failures you’ve handled (e.g., 4xx you won’t fix by retrying).
Canceling or updating work
When a user logs out or disables sync:
Future<void> disableSync() async {
await Workmanager().cancelByUniqueName('syncTask');
await Workmanager().cancelByUniqueName('dailySync');
}
To change cadence (e.g., from hourly to every 3 hours), cancel and re‑register with the new interval.
Testing and debugging
- Initialize with isInDebugMode: true to make scheduling more eager in debug builds.
- On Android, use a small frequency in debug and watch logs (adb logcat) for WorkManager execution.
- On iOS, run on a device and use Xcode → Debug → Simulate Background Fetch/Processing. Observe console logs.
- Create a dedicated “one‑off” test task that you can fire from a debug menu.
Common pitfalls and how to avoid them
- Expectation mismatch: Periodic ≠ exact. Communicate “automatic background updates” to users, not precise times.
- Long‑running work: Split into chunks; each run should be bounded. Persist progress between runs.
- UI‑bound plugins in background: Many plugins assume a foreground UI thread. Keep background logic pure Dart or use plugins known to support background isolates.
- Unmarked entry points: Always add @pragma(‘vm:entry-point’) to your dispatcher.
- Battery killers: Don’t poll the network aggressively. Use deltas and server‑driven hints.
Security and privacy
- Use HTTPS everywhere; pin domains if appropriate.
- Store credentials securely (OS keychain/keystore). Background isolates read the same secure storage, but ensure your library supports background contexts.
- Respect user consent: a settings toggle for “Background sync” with a clear explanation helps trust and may be required by policy.
Observability
- Record lightweight metrics: last success time, duration bucket, records uploaded/downloaded, last error code.
- Expose a read‑only “Sync status” screen pulling from local metrics, not live background state.
Alternatives and complements
- Foreground refresh on app open or pull‑to‑refresh for immediacy.
- Push‑to‑sync: send silent push notifications to nudge background work (platform policies apply; not guaranteed delivery).
- Other plugins: background_fetch or platform‑specific schedulers. For most apps, workmanager is the right default due to its resilience and OS alignment.
Checklist for production
- Clear, user‑visible setting for background sync and data usage.
- Conservative default interval; loosen only when necessary.
- Idempotent, chunked sync with robust error handling.
- Constraints tuned for your use case (network, battery‑not‑low, charging if heavy).
- iOS capabilities enabled and identifiers whitelisted in Info.plist.
- Cancellation on logout and re‑registration on login.
- Telemetry for success/failure and last run time.
Final thoughts
workmanager gives Flutter apps a pragmatic path to reliable background work. Embrace the platform constraints, design an idempotent and chunked sync pipeline, and be upfront with users about timing. Do that, and your app will feel fast and fresh—without draining batteries or fighting the OS.
Related Posts
Flutter WorkManager Scheduled Tasks: The Complete, Practical Guide (2026)
A practical guide to scheduling background tasks in Flutter with WorkManager on Android and iOS, with setup, periodic jobs, constraints, and debugging.
Flutter WorkManager: Reliable Background Tasks on Android and iOS
A practical guide to Flutter WorkManager for background tasks: setup, scheduling, constraints, iOS limits, testing, and best practices.
Flutter background fetch periodic tasks: reliable patterns for Android and iOS
Learn how to run periodic background tasks in Flutter with WorkManager and iOS BackgroundTasks, plus reliable patterns, caveats, and code examples.