easy_sync 0.2.2 copy "easy_sync: ^0.2.2" to clipboard
easy_sync: ^0.2.2 copied to clipboard

Flutter background sync and offline-first sync orchestration with Workmanager, retry, preconditions, rate limiting, and circuit breaker controls.

easy_sync #

pub package likes popularity

easy_sync is a Flutter background sync and offline sync orchestration package.

It helps you run reliable app-open, manual, and background sync flows with workmanager, retry, preconditions, and execution controls such as rate limit and circuit breaker.

Overview #

easy_sync helps you organize sync work into reusable tasks.

It is designed for apps that need to:

  • run sync on app open
  • trigger sync manually from the UI
  • schedule background sync with workmanager
  • retry transient failures with backoff
  • track task state in a predictable way

The package is:

  • auth-agnostic
  • backend-agnostic
  • database-agnostic
  • reusable across different Flutter apps

Use Cases #

Use easy_sync when your app needs:

  • offline-first sync from local database to remote API
  • queued uploads and retry after transient failures
  • app-open refresh and pull-to-refresh with shared orchestration
  • background periodic sync with WorkManager and iOS fetch/processing modes
  • backend protection through rate limiting and circuit breaker behavior

Installation #

Add the package to your app:

dependencies:
  easy_sync: ^0.2.0

Then install dependencies:

flutter pub get

If you enable background sync, complete the platform setup in the Native Setup For Background Sync section below.

Quick Start #

Set up easy_sync with a minimal task:

import 'package:easy_sync/easy_sync.dart';

Future<void> example() async {
  final easySync = await EasySync.setup(
    tasks: <SyncTask>[
      SyncTask.fn(
        key: 'sync_users',
        manual: true,
        appOpen: true,
        background: true,
        run: (context) async {
          // Call your repository or API client here.
        },
      ),
    ],
    appOpenSync: true,
    background: EasySyncBackgroundConfig.enabled(
      frequency: const Duration(hours: 1),
    ),
    taskTimeout: const Duration(seconds: 20),
    isolateTaskFailures: true,
  );

  await easySync.runAll(
    metadata: const <String, Object?>{
      'source': 'manual',
      'hasNetwork': true,
    },
  );

  await easySync.dispose();
}

See the Full Example below for a production-ready setup with retry handling, preconditions, and background sync configuration.

Follow this simple flow in your app:

  1. Define your SyncTask classes
  2. Call EasySync.setup(...) during app startup (usually in main())
  3. Pass the returned EasySync instance into your app
  4. Trigger manual sync from the UI when needed (runAll() / runTask())
  5. Let easy_sync handle app-open and background sync automatically

Full Example (main.dart) #

This example shows a practical startup flow.

import 'package:flutter/material.dart';
import 'package:easy_sync/easy_sync.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  Future<void> uploadPendingItems() async {
    // Call your repository or API layer.
  }

  Future<String?> readAccessToken() async {
    // Read from your auth module.
    return 'token';
  }

  final easySync = await EasySync.setup(
    // 1) Define your app tasks.
    tasks: <SyncTask>[
      SyncTask.fn(
        key: 'upload_pending_items',
        appOpen: true,
        manual: true,
        background: true,
        retry: RetryConfig.exponential(
          initialDelay: Duration(seconds: 1),
          maxRetries: 4,
        ),
        preconditions: <SyncPrecondition>[
          RequiresNetworkPrecondition(
            checker: (context) async =>
                context.value<bool>('hasNetwork') ?? false,
          ),
          PredicatePrecondition(
            name: 'auth-ready',
            predicate: (context) async => await readAccessToken() != null,
          ),
        ],
        run: (context) async {
          await uploadPendingItems();
        },
        retryWhen: (error, stackTrace) => true,
      ),
    ],
    // 2) Automatically trigger app-open sync on start and resume.
    appOpenSync: true,
    // 3) Enable background sync with the common configuration path.
    background: EasySyncBackgroundConfig.enabled(
      frequency: const Duration(hours: 1),
    ),
    // 4) Optional execution safety settings.
    taskTimeout: const Duration(seconds: 20),
    isolateTaskFailures: true,
  );

  // 5) The app only needs the returned EasySync instance.
  runApp(MyApp(easySync: easySync));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key, required this.easySync});

  final EasySync easySync;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('easy_sync example')),
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              // 6) Manual sync stays simple.
              await easySync.runAll(
                metadata: const <String, Object?>{
                  'source': 'button_tap',
                  'hasNetwork': true,
                },
              );
            },
            child: const Text('Run Manual Sync'),
          ),
        ),
      ),
    );
  }
}

App Open Sync #

For the common case, set appOpenSync: true in EasySync.setup().

final easySync = await EasySync.setup(
  tasks: tasks,
  appOpenSync: true,
);

This automatically:

  • triggers app-open tasks once on startup
  • listens for AppLifecycleState.resumed
  • triggers app-open tasks again when the app returns to foreground

Manual Sync #

Use the returned EasySync instance from UI interactions such as pull-to-refresh or a button tap.

await easySync.runAll(
  metadata: const <String, Object?>{
    'source': 'pull_to_refresh',
    'hasNetwork': true,
  },
);

await easySync.runTask(
  'upload_pending_items',
  metadata: const <String, Object?>{
    'source': 'retry_button',
    'hasNetwork': true,
  },
);

Background Sync #

For the common case, provide EasySyncBackgroundConfig.enabled() to EasySync.setup().

final easySync = await EasySync.setup(
  tasks: tasks,
  background: EasySyncBackgroundConfig.enabled(
    frequency: const Duration(hours: 1),
  ),
);

On iOS, this uses the BGTaskScheduler periodic path (custom frequency, processing mode).

For iOS Background Fetch mode (system-managed timing, no custom frequency), use:

final easySync = await EasySync.setup(
  tasks: tasks,
  background: EasySyncBackgroundConfig.iosBackgroundFetch(),
);

To explicitly disable background scheduling while keeping the rest of setup() the same:

final easySync = await EasySync.setup(
  tasks: tasks,
  background: EasySyncBackgroundConfig.disabled(),
);

Keep in mind:

  • Android uses WorkManager semantics.
  • iOS supports two paths via workmanager:
    • BGTaskScheduler periodic (EasySyncBackgroundConfig.enabled(...) / .periodic(...))
    • Background Fetch (EasySyncBackgroundConfig.iosBackgroundFetch())
  • Android periodic work has a practical 15 minute minimum interval behavior.
  • If you pass less than 15 minutes, easy_sync clamps it to 15 minutes.
  • background timing is not guaranteed
  • iOS background timing is especially best-effort

Native Setup For Background Sync #

Use these steps before enabling background sync through EasySync.setup(...).

Android #

Android setup is the simple part.

  1. Add easy_sync to your app.
  2. Run flutter pub get.
  3. Make sure your app uses Flutter's default generated Android setup.
  4. No extra Android manifest or Application class setup is usually needed for basic workmanager usage.

In most apps, Android works after Dart-side initialization only.

iOS #

iOS needs explicit native setup.

  1. Open ios/Runner.xcworkspace in Xcode.
  2. Select the Runner target.
  3. Set the minimum deployment target to iOS 14.0 or later.
  4. Open Signing & Capabilities.
  5. Add Background Modes.
  6. Enable the background mode that matches your scheduling approach.

Option A: Background Fetch (simpler, no custom frequency)

Use this if you call EasySyncBackgroundConfig.iosBackgroundFetch().

Add these keys in ios/Runner/Info.plist:

<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
</array>

No AppDelegate registration is needed for this mode.

Option B: BGTaskScheduler periodic (custom frequency)

Use this if you call EasySyncBackgroundConfig.enabled(...) or .periodic(...).

For periodic background sync with workmanager, use BGTaskScheduler-style setup:

Add these keys in ios/Runner/Info.plist:

<key>UIBackgroundModes</key>
<array>
    <string>processing</string>
</array>

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
  <!-- Example: if your app id is ca.devsloom.testapp -->
  <string>ca.devsloom.testapp.sync-background</string>
</array>

Then register the same identifier in ios/Runner/AppDelegate.swift:

import UIKit
import Flutter
import workmanager_apple

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    // Use your own app's bundle-style identifier here.
    // Example:
    // if your app id is ca.devsloom.testapp
    // then use ca.devsloom.testapp.sync-background
    WorkmanagerPlugin.registerPeriodicTask(
      withIdentifier: "ca.devsloom.testapp.sync-background",
      frequency: NSNumber(value: 20 * 60)
    )

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Keep these rules in mind:

  • Use the same identifier in Info.plist and AppDelegate.swift.
  • A safe pattern is: <your-app-id>.sync-background.
  • iOS background execution is best-effort.
  • Exact timing is not guaranteed.
  • Real devices are more reliable than simulators for background testing.

If you want the most up-to-date native details, check the workmanager quick start as well, because platform requirements can change between plugin versions.

Advanced Usage #

The high-level EasySync.setup() API is meant for the common integration path.

Use the lower-level APIs when you need custom control over:

  • task registration creation
  • state store lifecycle
  • app-open scheduling behavior
  • background bridge registration
  • scheduler initialization timing

Available lower-level APIs:

  • EasySync.initialize(...)
  • EasySyncBackgroundConfig.periodic(...)
  • EasySyncBackgroundConfig.iosBackgroundFetch(...)
  • SyncEngine
  • SyncTaskRegistration
  • WorkmanagerSyncBridge.registerTaskMapping(...)
  • WorkmanagerBackgroundScheduler.initialize()
  • WorkmanagerBackgroundScheduler.schedulePeriodic(...)
  • WorkmanagerBackgroundScheduler.isScheduledByUniqueName(...) (Android only)

Example:

final easySync = EasySync.initialize(
  tasks: tasks,
  stateStore: InMemorySyncTaskStateStore(),
);

final taskRegistrations = <SyncTaskRegistration>[
  for (final task in tasks) SyncTaskRegistration(task: task),
];

WorkmanagerSyncBridge.registerTaskMapping(
  taskName: 'sync-background',
  taskRegistrations: taskRegistrations,
  stateStoreFactory: InMemorySyncTaskStateStore.new,
);

final scheduler = WorkmanagerBackgroundScheduler();
await scheduler.initialize();
await scheduler.schedulePeriodic(
  uniqueName: 'easy-sync-periodic',
  taskName: 'sync-background',
  frequency: const Duration(hours: 1),
);

Use EasySyncBackgroundConfig.periodic(...) when you need to customize values such as:

  • uniqueName
  • taskName
  • inputData
  • stateStoreFactory
  • initialDelay
  • custom scheduler driver for advanced integrations or testing

Core Concepts #

  • SyncTask: a uniquely keyed unit of sync work
  • SyncPolicy: controls whether a task can run on app-open, manual, or background triggers
  • SyncTaskHandler: contains the actual sync implementation
  • SyncPrecondition: blocks execution until requirements are met
  • SyncResult: reports success, failure, or retryable failure
  • SyncTaskState: stores the latest known runtime state for a task
  • EasySync: convenience API for manual sync and state streaming
  • WorkmanagerSyncBridge: connects workmanager callbacks to your task registrations

Preconditions #

Preconditions decide whether a task is allowed to run.

Use them for checks such as:

  • network availability
  • authentication readiness
  • account state
  • feature flags

Example:

class AuthReadyPrecondition implements SyncPrecondition {
  AuthReadyPrecondition(this.readAccessToken);

  final Future<String?> Function() readAccessToken;

  @override
  String get name => 'auth-ready';

  @override
  Future<PreconditionResult> check(SyncContext context) async {
    final token = await readAccessToken();
    if (token == null) {
      return PreconditionResult.blocked(reason: 'Missing access token');
    }
    return PreconditionResult.allow();
  }
}

Retry #

Retries are controlled by SyncPolicy.retry and only happen when your handler returns SyncResult.retryable(...).

@override
SyncPolicy get policy => const SyncPolicy(
      manual: true,
      background: true,
      retry: RetryConfig.exponential(
        initialDelay: Duration(seconds: 1),
        maxRetries: 4,
      ),
    );

This produces delays like:

  • 1s
  • 2s
  • 4s
  • 8s

Use retry only for transient failures such as:

  • temporary network issues
  • short-lived server errors
  • temporary dependency failures

Rate Limit and Circuit Breaker #

Use execution controls when you want to protect your backend during instability or high trigger volume.

final easySync = await EasySync.setup(
  tasks: tasks,
  rateLimit: const SyncRateLimit.slidingWindow(
    maxExecutions: 5,
    per: Duration(minutes: 1),
  ),
  circuitBreaker: const SyncCircuitBreaker.standard(
    failureThreshold: 3,
    openFor: Duration(minutes: 5),
  ),
);

How it works:

  • rate limit blocks task execution when attempts exceed the configured sliding window
  • circuit breaker opens after repeated failures and temporarily blocks further execution
  • once the open duration passes, execution is allowed again

State Tracking #

Use stateStream to observe task changes.

final subscription = easySync.stateStream.listen((state) {
  print(
    'task=${state.taskKey} status=${state.status.name} attempt=${state.attempt}',
  );
});

await subscription.cancel();

This is useful for:

  • loading indicators
  • sync history UI
  • retry messaging
  • debug logging

Platform Notes #

  • Android background work follows WorkManager behavior.
  • iOS background work can use BGTaskScheduler periodic mode or Background Fetch through workmanager.
  • Some devices and OS versions may delay or skip background work.
  • For native setup details, use the workmanager package documentation.

Limitations #

  • easy_sync does not provide authentication.
  • easy_sync does not provide API clients.
  • easy_sync does not provide local database integration.
  • background execution timing is not guaranteed
  • iOS background execution is best-effort and may be infrequent

FAQ #

Does easy_sync include authentication?

  • No. It is intentionally auth-agnostic.

Does easy_sync include API client or database code?

  • No. It is backend-agnostic and database-agnostic.

Where should I call EasySync.setup()?

  • Near app startup, usually in main() before runApp().

Where should I configure background sync?

  • During app startup, before scheduling periodic work.

When should I trigger app-open sync?

  • Set appOpenSync: true in EasySync.setup() for the common case.

How do I sync local database data in background with Dio?

  • Read pending records inside your task run function, call your Dio API client, then mark records as synced. Re-open required dependencies in background execution context.

Should I use iOS Background Fetch or BGTaskScheduler periodic mode?

  • Use EasySyncBackgroundConfig.iosBackgroundFetch() for simpler setup and system-managed timing. Use EasySyncBackgroundConfig.enabled(...) or .periodic(...) when you need BGTaskScheduler periodic behavior with custom frequency.

Can I protect my backend from sync spikes?

  • Yes. Configure SyncRateLimit and SyncCircuitBreaker in EasySync.setup(...) to block excessive execution bursts and temporarily open-circuit on repeated failures.
0
likes
160
points
46
downloads

Documentation

Documentation
API reference

Publisher

verified publisherdevsloom.ca

Weekly Downloads

Flutter background sync and offline-first sync orchestration with Workmanager, retry, preconditions, rate limiting, and circuit breaker controls.

Repository (GitHub)
View/report issues

Topics

#flutter #synchronization #background-tasks #offline-first #workmanager

License

MIT (license)

Dependencies

flutter, meta, workmanager

More

Packages that depend on easy_sync