Background tasks for Flutter β 25+ built-in workers, zero Flutter Engine overhead.
HTTP, file ops, image processing, encryption β all in pure Kotlin & Swift.
// 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.imageResize(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.
1. Add the dependency:
dependencies:
native_workmanager: ^1.2.02. 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_iosThe dominant workmanager plugin spins up a full Flutter Engine per background task: ~50β100 MB RAM, up to 3 seconds cold start, a Dart isolate the OS kills the moment memory gets tight. On Xiaomi/Samsung/Huawei devices with aggressive battery optimization, the engine never even starts.
native_workmanager runs tasks as pure Kotlin coroutines and Swift async functions β no engine, no isolate, no cold-start penalty.
workmanager |
native_workmanager |
|
|---|---|---|
| Memory per task | ~50β100 MB | ~2β5 MB |
| Task startup | 1,500β3,000 ms | < 50 ms |
| 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) |
| Per-task progress stream | β | β |
| 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.
All workers run natively. No Flutter Engine. No setup beyond initialize().
| Category | Workers |
|---|---|
| HTTP | httpDownload (resumable), httpUpload (multipart), parallelDownload (chunked), httpSync, httpRequest |
| Image | imageResize, imageCrop, imageConvert, imageThumbnail β all EXIF-aware |
pdfMerge, pdfCompress, imagesToPdf |
|
| Crypto | cryptoEncrypt (AES-256-GCM), cryptoDecrypt, cryptoHash (SHA-256/512), hmacSign |
| File | fileCopy, fileMove, fileDelete, fileList |
| Storage | moveToSharedStorage (Android MediaStore / iOS Files app) |
| Real-time | webSocket β Android |
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')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.
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.
Android killed-app support β When Android kills your app and WorkManager later fires a
DartWorker, the process restarts without Flutter. The plugin automatically restores thecallbackHandlefromSharedPreferences, but yourApplicationclass must implementConfiguration.Providerso WorkManager uses the plugin'sWorkerFactory. One-time setup β see Android Setup Guide.
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| 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 |
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.
π₯ 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)),
constraints: const Constraints(requiresNetwork: true),
existingPolicy: ExistingTaskPolicy.keep,
);πΈ Photo backup pipeline
await NativeWorkManager
.beginWith(TaskRequest(
id: 'compress',
worker: NativeWorker.imageResize(
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();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}');
}
});| Guide | Description |
|---|---|
| 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 |
- 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.