native_workmanager

native_workmanager

Background tasks for Flutter — 25+ built-in workers, zero Flutter Engine overhead.
HTTP, file ops, image processing, encryption — all in pure Kotlin & Swift.

pub.dev Pub Points CI MIT Android 8.0+ iOS 14.0+


The 30-second pitch

// Download → resize → upload — survives app kill, device reboot, low memory
await NativeWorkManager
  .beginWith(TaskRequest(id: 'dl',
    worker: NativeWorker.httpDownload(url: photoUrl, savePath: '/tmp/raw.jpg')))
  .then(TaskRequest(id: 'resize',
    worker: NativeWorker.imageProcess(inputPath: '/tmp/raw.jpg',
      outputPath: '/tmp/thumb.jpg', maxWidth: 512)))
  .then(TaskRequest(id: 'upload',
    worker: NativeWorker.httpUpload(url: uploadUrl, filePath: '/tmp/thumb.jpg')))
  .named('photo-pipeline')
  .enqueue();

No boilerplate. No native code to write. No AndroidManifest.xml changes. Each step retries independently — if the upload fails, only the upload retries.


Quick Start

1. Add the dependency:

dependencies:
  native_workmanager: ^1.8.0

2. Initialize once in main():

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NativeWorkManager.initialize();
  runApp(MyApp());
}

3. Schedule a background task:

await NativeWorkManager.enqueue(
  taskId: 'daily-sync',
  worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
  constraints: const Constraints(requiresNetwork: true),
);

iOS only — run once to configure BGTaskScheduler automatically:

dart run native_workmanager:setup_ios

Flutter 3.38+ / UIScene apps: fully supported since v1.3.2 — the plugin registers its BGTask launch handlers in an ObjC +load hook, before the app finishes launching, so the new UIScene plugin-registration timing cannot crash it (see iOS Setup Guide §4b and Issue #36). No extra AppDelegate code is required on either the old or the new template.


Why developers switch from workmanager

The dominant workmanager plugin spins up a full Flutter Engine per background task — a Dart isolate that costs memory and cold-start time, and that the OS targets first when memory gets tight.

native_workmanager runs tasks as pure Kotlin coroutines and Swift async functions — no engine, no isolate, no cold-start penalty.

The table below lists capability differences only. It carries no RAM or latency figures: those were previously quoted here without a reproducible measurement behind them, so they were removed rather than restated. See §5.2 of the Best Practices guide for how to measure them on your own hardware.

workmanager native_workmanager
Boots a Flutter engine per task ✅ Always ❌ Never (Mode 1)
Task restored after process death ⚠️ Partial ✅ (WorkManager/SQLite + BGTaskScheduler persistence)
Battery-restriction diagnostics ✅ (batteryRestriction())
Built-in HTTP workers ✅ (resumable download, chunked upload, parallel)
Built-in image workers ✅ (resize, crop, convert, thumbnail — EXIF-aware)
Built-in crypto workers ✅ (AES-256-GCM, SHA-256/512, HMAC)
Task chains (A→B→C) ✅ (persist across reboots)
FGS Bypass (Android) ✅ (Bypass Doze/Standby with custom notifications)
Per-task progress stream
Retry ceiling (maxRetries) ❌ (unbounded Result.retry()) ✅ (capped on both platforms)
Survives device reboot
Remote Trigger (Push) ✅ (FCM/APNs + HMAC Security)
Custom Dart workers ✅ (opt-in via DartWorker)

If you only do HTTP syncs and file ops, you probably don't need Dart workers at all. Use the native workers directly — they're production-hardened and need zero engine overhead.

📖 Deep Dive: Read the Architecture & Best Practices Guide for the dual-mode decision tree, reliability patterns, and the current state of performance measurement.


Industrial-Grade OOM Resilience

Most Flutter background libraries boot a full Flutter Engine for every task. Under memory pressure, the OS kills the heaviest processes first.

native_workmanager uses a Zero-Engine Architecture: Native Workers run in pure Kotlin/Swift and never start an engine at all, so there is no engine to be killed. That is a structural difference you can verify by reading NativeWorker — no Dart isolate is created on the Mode 1 path.

On numbers: this README used to quote "~2 MB vs 50 MB+". Those figures were not produced by a run anyone could reproduce, so they have been removed rather than restated. See §5.2 of the Best Practices guide for what will replace them and how to measure it yourself today.

The OOM Survival Test

Even if the system is under extreme pressure and kills your app while a task is running, your work is not lost.

  • Android: Managed by WorkManager with system-level persistence. Tasks are automatically rescheduled with exponential backoff.
  • iOS: Recovers state from a native SQLite store the moment a new background window is granted.

See for yourself: Run the "Simulate OOM Kill" demo in the example app. It crashes the app with a memory bomb, and you'll see the background task trigger successfully seconds later.


25+ Built-in Workers

All workers run natively. No Flutter Engine. No setup beyond initialize().

Category Workers
HTTP httpDownload (resumable), httpUpload (multipart), multiUpload, parallelHttpDownload (chunked), httpSync, httpRequest
Image imageProcess — resize, crop, convert format, thumbnail (all via one EXIF-aware worker)
PDF pdfMerge, pdfCompress, pdfFromImages
Crypto cryptoEncrypt (AES-256-GCM), cryptoDecrypt, hashFile / hashString (SHA-256/512)
File fileCopy, fileMove, fileDelete, fileList, fileMkdir, fileCompress, fileDecompress
Storage moveToSharedStorage (Android MediaStore / iOS Files app)
Real-time webSocket — Android
Custom custom (bring your own native worker class), DartWorker (opt-in Dart callback)

TLS Certificate Pinning

Opt-in, per-request, on every HTTP-ish worker. Pin one or more hosts to their SHA-256 SubjectPublicKeyInfo digest — the sha256/BASE64 form OkHttp, TrustKit and openssl all emit — and a worker that never sets it behaves exactly as before, unaffected:

NativeWorker.httpRequest(
  url: 'https://api.example.com/data',
  certificatePinning: CertificatePinning([
    CertificatePin(
      hostname: 'api.example.com',
      sha256Pins: [
        'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // current
        'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // backup — see below
      ],
    ),
  ]),
)

Pinning is additional to the platform's own chain validation, never a replacement for it — a matching pin still results in the OS performing its own expiry, hostname and trust-store checks.

Ship a backup pin. This is the failure that makes teams abandon pinning, and it isn't recoverable remotely: pin only the key in use today, rotate it, and every installed copy of the app loses the ability to reach that host — there is no server-side fix. Pin at least one key not in use yet (typically the intermediate CA, or a backup key held offline).

Works on HttpRequestWorker, HttpDownloadWorker, HttpUploadWorker, HttpSyncWorker, ParallelHttpDownloadWorker, ParallelHttpUploadWorker and WebSocketWorker (Android). Not applied to HttpDownloadWorker's useBackgroundSession: true path on iOS — that hands off to a single app-lifetime background session shared across every background download, which per-request pinning can't reach there.


Track progress in real time

enqueue() returns a TaskHandler that streams progress and completion events for that specific task — no manual filtering required.

final handler = await NativeWorkManager.enqueue(
  taskId: 'big-download',
  worker: NativeWorker.httpDownload(
    url: 'https://cdn.example.com/video.mp4',
    savePath: '/tmp/video.mp4',
  ),
);

// Stream progress for this task only
handler.progress.listen((p) {
  print('${p.progress}% — ${p.networkSpeedHuman} — ETA ${p.timeRemainingHuman}');
});

// Await completion
final result = await handler.result;
print(result.success ? 'Done!' : 'Failed: ${result.message}');

Or drop in the built-in widget:

TaskProgressCard(handler: handler, title: 'Downloading video')

Task Chains

Chain workers into persistent pipelines. Each step only runs when the previous one succeeds, and the entire chain survives app kills and device reboots (SQLite-backed state).

await NativeWorkManager
  .beginWith(TaskRequest(
    id: 'download',
    worker: NativeWorker.httpDownload(
      url: 'https://cdn.example.com/report.pdf',
      savePath: '/tmp/report.pdf',
    ),
  ))
  .then(TaskRequest(
    id: 'encrypt',
    worker: NativeWorker.cryptoEncrypt(
      inputPath: '/tmp/report.pdf',
      outputPath: '/tmp/report.enc',
      password: vaultKey,
    ),
  ))
  .then(TaskRequest(
    id: 'upload',
    worker: NativeWorker.httpUpload(
      url: 'https://vault.example.com/store',
      filePath: '/tmp/report.enc',
    ),
  ))
  .named('secure-report-pipeline')
  .enqueue();

Use .thenAll([...]) to run tasks in parallel, then continue the chain when all finish.


Custom Dart Workers

For app-specific logic that must run in Dart, register a top-level function as a background worker:

@pragma('vm:entry-point')
Future<bool> syncHealthData(Map<String, dynamic>? input) async {
  final userId = input?['userId'] as String?;
  await uploadHealthMetrics(userId);
  return true;
}

// Register once at startup
NativeWorkManager.registerDartWorker('health-sync', syncHealthData);

// Schedule it
await NativeWorkManager.enqueue(
  taskId: 'sync-user-42',
  worker: DartWorker(callbackId: 'health-sync', input: {'userId': '42'}),
);

Dart workers boot a headless Flutter isolate (~50 MB, 1–2 s cold start). The isolate is cached for 5 minutes so back-to-back tasks pay the boot cost only once. For HTTP and file tasks, use native workers instead.

Retry on failure — return false (or throw) and the task retries under Constraints.maxRetries and backoffPolicy/backoffDelayMs, then fails permanently once the ceiling is hit (maxRetries: 3 by default → up to 4 total runs, enforced on both platforms since v1.4.0). Use Constraints(maxRetries: 0) for fail-fast tasks that must never re-run:

await NativeWorkManager.enqueue(
  taskId: 'sync-user-42',
  worker: DartWorker(callbackId: 'health-sync', input: {'userId': '42'}),
  constraints: const Constraints(
    maxRetries: 2,                        // 1 initial run + 2 retries max
    backoffPolicy: BackoffPolicy.linear,
    backoffDelayMs: 30000,
  ),
);

Report progress from inside a DartWorker and it streams to NativeWorkManager.progress / handler.progress just like a native worker (Android and iOS):

@pragma('vm:entry-point')
Future<bool> syncHealthData(Map<String, dynamic>? input) async {
  final taskId = input?['__taskId'] as String?;
  await NativeWorkManager.reportDartWorkerProgress(taskId: taskId, progress: 50);
  // ... do work ...
  await NativeWorkManager.reportDartWorkerProgress(taskId: taskId, progress: 100);
  return true;
}

Cancelling a task (cancel/cancelAll, or the OS reclaiming background time) does not interrupt a running DartWorker callback — Dart has no API to preemptively abort a Future that is already executing. A callback doing long-running work should poll NativeWorkManager.isTaskCancelled(taskId) cooperatively between chunks of work and return promptly once it turns true:

@pragma('vm:entry-point')
Future<bool> longSync(Map<String, dynamic>? input) async {
  final taskId = input?['__taskId'] as String?;
  for (var i = 1; i <= 100; i++) {
    if (taskId != null && await NativeWorkManager.isTaskCancelled(taskId)) {
      return false; // bail out — do not keep working
    }
    await processChunk(i);
  }
  return true;
}

An await longRunningOperation() with no cancellation checks of its own keeps running regardless — break such work into chunks so there's a point to check from.

Android killed-app support — When Android kills your app and WorkManager later fires a DartWorker, the process restarts without Flutter. Since v1.3.0 this is zero-config: the plugin's androidx.startup initializer restores the callbackHandle and installs its WorkerFactory automatically before any task fires — no custom Application class required. Apps that ship their own Configuration.Provider can opt out — see Android Setup Guide.

Code generation for DartWorker

The companion native_workmanager_gen package generates type-safe callback IDs and a worker registry from @WorkerCallback annotations, eliminating string-based registration and magic IDs:

@WorkerCallback('health-sync')
Future<bool> syncHealthData(Map<String, dynamic>? input) async { ... }

// Generated: WorkerCallbacks.healthSync, auto-registered in WorkerRegistry

By default, native_workmanager runs with registerPlugins: false. This follows our Zero-Engine I/O principle to avoid loading plugins the background task never uses, and to prevent hardware side-effects (like Bluetooth or Audio disconnects when a background task finishes).

If your DartWorker needs to use other plugins (e.g., flutter_local_notifications, shared_preferences), you should register them selectively on the native side. This is more efficient and stable than registering all plugins.

1. Android (Kotlin)

In your MainActivity.kt or MainApplication.kt:

import dev.brewkits.native_workmanager.NativeWorkmanagerPlugin
import io.flutter.embedding.engine.FlutterEngine
import com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin 

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        NativeWorkmanagerPlugin.setPluginRegistrantCallback(object : NativeWorkmanagerPlugin.Companion.PluginRegistrantCallback {
            override fun registerWith(engine: FlutterEngine) {
                // Register ONLY the plugins you need in background
                engine.plugins.add(FlutterLocalNotificationsPlugin())
            }
        })
    }
}

2. iOS (Swift)

In your AppDelegate.swift:

import native_workmanager
import flutter_local_notifications

@main
@objc class AppDelegate: FlutterAppDelegate {
    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        GeneratedPluginRegistrant.register(with: self)
        
        NativeWorkmanagerPlugin.setPluginRegistrantCallback { registry in
            // Manual registration for background engine
            FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")!)
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

3. Flutter (Dart)

Keep registerPlugins: false to maintain peak performance:

await NativeWorkManager.initialize(
  registerPlugins: false, // Lean background engine
);

Platform Support

Feature Android iOS
One-time tasks
Periodic tasks ✅ (BGAppRefresh)
Exact-time triggers
Task chains (persistent)
Network / charging constraints
Per-task progress stream
Foreground service (long tasks)
Custom Dart workers
Min OS version Android 8.0 (API 26) iOS 14.0

Migrating from workmanager

Most migrations take under 10 minutes. The conceptual model is the same; the API is a strict superset.

workmanager native_workmanager
Workmanager().initialize(...) NativeWorkManager.initialize()
Workmanager().registerOneOffTask(...) NativeWorkManager.enqueue(worker: NativeWorker.httpSync(...))
Workmanager().registerPeriodicTask(...) NativeWorkManager.enqueue(trigger: TaskTrigger.periodic(...))
Custom Dart callback DartWorker(callbackId: ...)

See Migration Guide for a step-by-step walkthrough.


Common Use Cases

📥 Resumable large file download
await NativeWorkManager.enqueue(
  taskId: 'download-dataset',
  worker: NativeWorker.httpDownload(
    url: 'https://data.example.com/dataset.zip',
    savePath: '/tmp/dataset.zip',
    headers: {'Authorization': 'Bearer $token'},
    allowResume: true,
  ),
  constraints: const Constraints(requiresUnmeteredNetwork: true),
);
🔐 Encrypt & upload sensitive file
await NativeWorkManager
  .beginWith(TaskRequest(
    id: 'encrypt',
    worker: NativeWorker.cryptoEncrypt(
      inputPath: '/documents/report.pdf',
      outputPath: '/tmp/report.enc',
      password: securePassword,
    ),
  ))
  .then(TaskRequest(
    id: 'upload',
    worker: NativeWorker.httpUpload(
      url: 'https://vault.example.com/store',
      filePath: '/tmp/report.enc',
    ),
  ))
  .named('secure-backup')
  .enqueue();
⏱ Periodic background sync
await NativeWorkManager.enqueue(
  taskId: 'hourly-sync',
  worker: NativeWorker.httpSync(url: 'https://api.example.com/sync'),
  trigger: TaskTrigger.periodic(
    const Duration(hours: 1),
    initialDelay: const Duration(minutes: 30), // Delay first run by 30m
  ),
  constraints: const Constraints(requiresNetwork: true),
  existingPolicy: ExistingTaskPolicy.keep,
);

initialDelay on periodic tasks defers the first run instead of firing immediately on registration, saving resources when the app is first launched.

📸 Photo backup pipeline
await NativeWorkManager
  .beginWith(TaskRequest(
    id: 'compress',
    worker: NativeWorker.imageProcess(
      inputPath: photoPath,
      outputPath: '/tmp/photo_compressed.jpg',
      maxWidth: 1920,
      quality: 85,
    ),
  ))
  .then(TaskRequest(
    id: 'upload',
    worker: NativeWorker.httpUpload(
      url: 'https://backup.example.com/upload',
      filePath: '/tmp/photo_compressed.jpg',
    ),
  ))
  .named('photo-backup')
  .enqueue();

Listen to task events

NativeWorkManager.events.listen((event) {
  if (event.isStarted) {
    print('▶ ${event.taskId} started (${event.workerType})');
    return;
  }
  if (event.success) {
    print('✅ ${event.taskId} — ${event.resultData}');
  } else {
    print('❌ ${event.taskId} — ${event.message}');
  }
});

Documentation

Guide Description
Architecture & Best Practices Zero-Engine architecture, decision matrix, resilient patterns & competitor benchmarks
Getting Started Full setup walkthrough
API Reference All public types and methods
Android Setup Guide DartWorker killed-app persistence
iOS Setup Guide BGTaskScheduler details
Migration from workmanager Switch in under 10 minutes
Security SSRF, path traversal, data redaction
native_workmanager_gen Code generator for type-safe DartWorker callbacks

Support


MIT License · Made by BrewKits

Found this useful? A ⭐ on GitHub helps others discover it.

Libraries

native_workmanager
Native background task manager for Flutter.
testing
Testing utilities for native_workmanager.