native_workmanager 1.8.0
native_workmanager: ^1.8.0 copied to clipboard
Background task scheduling for Flutter — 25+ native workers (HTTP, image, crypto, file), task chains, zero Flutter Engine overhead.
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.
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
+loadhook, 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
WorkManagerwith 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) |
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'sandroidx.startupinitializer restores thecallbackHandleand installs itsWorkerFactoryautomatically before any task fires — no customApplicationclass required. Apps that ship their ownConfiguration.Providercan 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
🔌 Selective Plugin Registration (Recommended) #
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,
);
initialDelayon 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 #
- GitHub Issues — bugs and feature requests
- Discussions — questions and community help
MIT License · Made by BrewKits
Found this useful? A ⭐ on GitHub helps others discover it.