Estimated Time: 30 minutes for typical app API Compatibility: ~90% Difficulty: Easy
Memory Savings:
Your current usage (workmanager):
- 85 MB per task
- 10 tasks per day
- = 850 MB daily memory consumption
After migration (native_workmanager with native workers):
- 35 MB per task (or 5 MB with pure native workers)
- 10 tasks per day
- = 350 MB daily (or 50 MB with native workers)
Savings: 500-800 MB per day
Impact: Fewer crashes on low-end devices, better user reviews
Battery Savings:
24-hour test (periodic task every 15 minutes):
- workmanager: 7% battery drain
- native_workmanager: 3% battery drain
Savings: ~50% battery improvement
Impact: Higher App Store ratings, fewer user complaints
Performance Improvement:
- Faster task startup (native workers don't load Flutter Engine)
- Better responsiveness
- Less UI jank during background execution
These APIs work with minimal or no changes:
| workmanager | native_workmanager | Changes Needed |
|---|---|---|
Workmanager().initialize() |
NativeWorkManager.initialize() |
✅ Direct replacement |
registerOneOffTask() |
enqueue() with oneTime() |
|
registerPeriodicTask() |
enqueue() with periodic() |
|
cancelByUniqueName() |
cancel(taskId) |
✅ Direct replacement |
cancelAll() |
cancelAll() |
✅ Direct replacement |
| Constraints (network, battery) | Constraints(...) |
✅ Same concept, different syntax |
| workmanager | native_workmanager | Migration Path |
|---|---|---|
registerTask() (generic) |
enqueue() |
Use specific trigger type |
| Task tags | Not yet supported | Use individual cancel() calls (v1.1 will add tagging) |
| Input data (Map) | worker.input or DartWorker.input |
Restructure data passing |
| Callback dispatcher (switch/case) | Callback ID map | Refactor to map-based registration |
| Plugin registration | registerPlugins parameter |
Set registerPlugins: true if using other plugins in background |
| workmanager Feature | Alternative in native_workmanager |
|---|---|
| Background fetch (iOS specific) | Use TaskTrigger.periodic() |
| Custom callback dispatcher pattern | Use dartWorkers map registration |
We provide a tool to scan your codebase and generate a migration report:
# From your project root
dart run native_workmanager:migrateOutput:
📊 Migration Analysis Complete
Found: 12 background tasks
Compatibility: 90% (automatic migration possible)
Changes Required:
✅ 10 tasks → Automatic (registerOneOffTask, registerPeriodicTask)
⚠️ 2 tasks → Manual review needed (custom callbacks)
Generate migration code? (y/n)
> y
✅ Created: migration/
├── pubspec.yaml.new
├── main.dart.migrated
├── tasks.dart.migrated
└── MIGRATION_CHECKLIST.md
Next Steps:
1. Review generated code
2. Test in debug mode
3. Follow MIGRATION_CHECKLIST.md
Before:
dependencies:
workmanager: ^0.5.0After:
dependencies:
native_workmanager: ^1.2.2Then run:
flutter pub getNote: You can keep both packages temporarily during migration for gradual transition.
import 'package:workmanager/workmanager.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
Workmanager().initialize(
callbackDispatcher, // Top-level function
isInDebugMode: true
);
runApp(MyApp());
}
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) {
switch (task) {
case 'syncTask':
return syncData();
case 'uploadTask':
return uploadFiles();
default:
return Future.value(false);
}
});
}Option A: Native Workers Only (No Dart code in background)
import 'package:native_workmanager/native_workmanager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await NativeWorkManager.initialize();
runApp(MyApp());
}
// No callback dispatcher needed for native workers!Option B: With Dart Workers (Need Dart code in background)
import 'package:native_workmanager/native_workmanager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await NativeWorkManager.initialize(
registerPlugins: true, // Required if your callbacks use other plugins
dartWorkers: {
'syncTask': _syncDataCallback,
'uploadTask': _uploadFilesCallback,
},
); runApp(MyApp());
}
@pragma('vm:entry-point')
Future<bool> _syncDataCallback(Map<String, dynamic>? input) async {
// Your sync logic
return true;
}
@pragma('vm:entry-point')
Future<bool> _uploadFilesCallback(Map<String, dynamic>? input) async {
// Your upload logic
return true;
}Key Differences:
- No
callbackDispatcher()function - Callbacks registered as map (
'taskId': callbackFunction) - Add
@pragma('vm:entry-point')to prevent tree-shaking async/awaitsupported natively
Before (workmanager):
Workmanager().registerOneOffTask(
'task-1',
'syncTask',
inputData: {
'userId': 123,
'action': 'sync',
},
constraints: Constraints(
networkType: NetworkType.connected,
),
);After (native_workmanager with Native Worker):
await NativeWorkManager.enqueue(
taskId: 'task-1',
trigger: TaskTrigger.oneTime(),
worker: NativeWorker.httpSync(
url: 'https://api.example.com/sync?userId=123',
method: HttpMethod.post,
),
constraints: Constraints(
requiresNetworkType: NetworkType.connected,
),
);After (native_workmanager with Dart Worker - if you need Dart code):
await NativeWorkManager.enqueue(
taskId: 'task-1',
trigger: TaskTrigger.oneTime(),
worker: DartWorker(
callbackId: 'syncTask',
input: {
'userId': 123,
'action': 'sync',
},
autoDispose: true, // Release Flutter Engine after task
),
constraints: Constraints(
requiresNetworkType: NetworkType.connected,
),
);Before (workmanager):
Workmanager().registerPeriodicTask(
'periodic-sync',
'syncTask',
frequency: Duration(hours: 1),
constraints: Constraints(
networkType: NetworkType.unmetered,
),
);After (native_workmanager with Native Worker):
await NativeWorkManager.enqueue(
taskId: 'periodic-sync',
trigger: TaskTrigger.periodic(
Duration(hours: 1),
),
worker: NativeWorker.httpSync(
url: 'https://api.example.com/sync',
method: HttpMethod.post,
),
constraints: Constraints(
requiresNetworkType: NetworkType.unmetered,
),
);After (native_workmanager with Dart Worker):
await NativeWorkManager.enqueue(
taskId: 'periodic-sync',
trigger: TaskTrigger.periodic(
Duration(hours: 1),
),
worker: DartWorker(
callbackId: 'syncTask',
autoDispose: true,
),
constraints: Constraints(
requiresNetworkType: NetworkType.unmetered,
),
);Before (workmanager):
Workmanager().registerOneOffTask(
'upload-task',
'uploadTask',
backoffPolicy: BackoffPolicy.exponential,
backoffPolicyDelay: Duration(seconds: 30),
);After (native_workmanager):
await NativeWorkManager.enqueue(
taskId: 'upload-task',
trigger: TaskTrigger.oneTime(),
worker: NativeWorker.httpUpload(
url: 'https://api.example.com/upload',
filePath: '/path/to/file',
),
retryPolicy: RetryPolicy(
backoffPolicy: BackoffPolicy.exponential,
initialDelay: Duration(seconds: 30),
maxAttempts: 3,
),
);Before (workmanager):
constraints: Constraints(
networkType: NetworkType.unmetered,
requiresBatteryNotLow: true,
requiresCharging: true,
),After (native_workmanager - Same!):
constraints: Constraints(
requiresNetworkType: NetworkType.unmetered,
requiresBatteryNotLow: true,
requiresCharging: true,
),Note: Property name changed: networkType → requiresNetworkType
Before (workmanager):
// Cancel specific task
Workmanager().cancelByUniqueName('task-1');
// Cancel all tasks
Workmanager().cancelAll();
// Cancel by tag (if you used tags)
Workmanager().cancelByTag('sync-group');After (native_workmanager):
// Cancel specific task
await NativeWorkManager.cancel('task-1');
// Cancel all tasks
await NativeWorkManager.cancelAll();
// Cancel by tag - NOT YET SUPPORTED (coming in v1.1)
// Workaround: Track task IDs yourself and cancel individually
List<String> syncTasks = ['task-1', 'task-2', 'task-3'];
for (var taskId in syncTasks) {
await NativeWorkManager.cancel(taskId);
}Testing Checklist:
- All tasks schedule successfully
- Tasks execute in background (kill app and wait)
- Constraints work as expected (test Wi-Fi, charging, etc.)
- Retry logic works (simulate failures)
- Task cancellation works
- No crashes or memory leaks
- Monitor memory usage (should be lower)
- Test on low-end Android devices (biggest impact)
- Test on iOS (verify 30-second limit compliance)
Debug Tools:
// Monitor all task events
NativeWorkManager.events.listen((event) {
print('📊 Task: ${event.taskId} - State: ${event.state}');
});
// Get all scheduled tasks
final tasks = await NativeWorkManager.getAllTasks();
print('Scheduled tasks: ${tasks.length}');Before (workmanager):
// 1. Register callback
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) {
if (task == 'apiSync') {
final response = await http.post(
Uri.parse('https://api.example.com/sync'),
headers: {'Authorization': 'Bearer TOKEN'},
);
return response.statusCode == 200;
}
return false;
});
}
// 2. Schedule task
Workmanager().registerPeriodicTask(
'sync',
'apiSync',
frequency: Duration(hours: 1),
);After (native_workmanager - Native Worker):
// 1. No callback needed!
// 2. Schedule task
await NativeWorkManager.enqueue(
taskId: 'sync',
trigger: TaskTrigger.periodic(Duration(hours: 1)),
worker: NativeWorker.httpRequest(
url: 'https://api.example.com/sync',
method: HttpMethod.post,
headers: {'Authorization': 'Bearer TOKEN'},
),
);Savings: 50 MB RAM, 400ms startup time, simpler code
Before (workmanager):
// Complex: Manual HTTP multipart, retry logic, etc.
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
if (task == 'upload') {
final file = File(inputData!['filePath']);
var request = http.MultipartRequest(
'POST',
Uri.parse('https://api.example.com/upload'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path));
var response = await request.send();
return response.statusCode == 200;
}
return false;
});
}After (native_workmanager - Native Worker):
await NativeWorkManager.enqueue(
taskId: 'upload',
trigger: TaskTrigger.oneTime(),
worker: NativeWorker.httpUpload(
url: 'https://api.example.com/upload',
filePath: '/path/to/file.jpg',
headers: {'Authorization': 'Bearer TOKEN'},
),
retryPolicy: RetryPolicy(maxAttempts: 3), // Built-in retry!
);Savings: 50 MB RAM, 80 lines of code → 10 lines
Before (workmanager):
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
if (task == 'processData') {
final db = await openDatabase('my_db.db');
final data = await db.query('items');
// Complex processing...
await db.close();
return true;
}
return false;
});
}
Workmanager().registerOneOffTask('process', 'processData');After (native_workmanager - Dart Worker):
// 1. Register callback in main()
await NativeWorkManager.initialize(
dartWorkers: {
'processData': _processDataCallback,
},
);
@pragma('vm:entry-point')
Future<bool> _processDataCallback(Map<String, dynamic>? input) async {
final db = await openDatabase('my_db.db');
final data = await db.query('items');
// Complex processing...
await db.close();
return true;
}
// 2. Schedule task
await NativeWorkManager.enqueue(
taskId: 'process',
trigger: TaskTrigger.oneTime(),
worker: DartWorker(
callbackId: 'processData',
autoDispose: true, // NEW: Release engine after task
),
);Benefits: Same functionality, better memory management with autoDispose
Before:
// Schedule multiple tasks
Workmanager().registerPeriodicTask('sync-1', 'syncTask');
Workmanager().registerPeriodicTask('sync-2', 'syncTask');
Workmanager().registerOneOffTask('upload-1', 'uploadTask');After:
// Use native workers for I/O tasks
final tasks = [
('sync-1', 'https://api.example.com/sync1'),
('sync-2', 'https://api.example.com/sync2'),
];
for (var (taskId, url) in tasks) {
await NativeWorkManager.enqueue(
taskId: taskId,
trigger: TaskTrigger.periodic(Duration(hours: 1)),
worker: NativeWorker.httpSync(url: url, method: HttpMethod.post),
);
}
// Dart worker for complex task
await NativeWorkManager.enqueue(
taskId: 'upload-1',
trigger: TaskTrigger.oneTime(),
worker: DartWorker(callbackId: 'uploadTask'),
);Before:
Workmanager().registerPeriodicTask('sync-1', 'syncTask', tag: 'sync-group');
Workmanager().registerPeriodicTask('sync-2', 'syncTask', tag: 'sync-group');
// Cancel all tasks with tag
Workmanager().cancelByTag('sync-group');After (Workaround until v1.1):
// Track task IDs manually
class TaskGroups {
static const syncGroup = ['sync-1', 'sync-2', 'sync-3'];
static const uploadGroup = ['upload-1', 'upload-2'];
}
// Schedule tasks
for (var taskId in TaskGroups.syncGroup) {
await NativeWorkManager.enqueue(taskId: taskId, /* ... */);
}
// Cancel by group
Future<void> cancelGroup(List<String> taskIds) async {
for (var taskId in taskIds) {
await NativeWorkManager.cancel(taskId);
}
}
await cancelGroup(TaskGroups.syncGroup);Note: v1.1 will add native task tagging support.
When possible, convert Dart workers to native workers:
❌ Suboptimal (Dart Worker - 50 MB):
DartWorker(callbackId: 'httpRequest')✅ Optimal (Native Worker - 5 MB):
NativeWorker.httpRequest(url: '...') // 10x improvement!When to use each:
- Native Worker: HTTP requests, file operations, simple I/O
- Dart Worker: Complex business logic, need Dart packages, existing code reuse
Enable automatic Flutter Engine disposal:
DartWorker(
callbackId: 'processData',
autoDispose: true, // 👈 Releases engine after task completes
)Impact: Prevents memory accumulation, especially for periodic tasks.
Before (Manual coordination):
// Task 1: Download
Workmanager().registerOneOffTask('download', 'downloadTask');
// Manually check in callback if download succeeded, then:
// Task 2: Process (requires custom state management)
// Task 3: Upload (requires even more state management)After (Automated with Task Chains):
NativeWorkManager.beginWith(
TaskRequest(id: 'download', worker: NativeWorker.httpDownload(/* ... */)),
)
.then(TaskRequest(id: 'process', worker: DartWorker(callbackId: 'process')))
.then(TaskRequest(id: 'upload', worker: NativeWorker.httpUpload(/* ... */)))
.enqueue();Benefits: Automatic dependency management, built-in retry, failure isolation.
Use this checklist to track your migration progress:
-
Pre-Migration
- Run migration analyzer tool
- Review generated report
- Backup current codebase
- Read this migration guide
-
Code Changes
- Update pubspec.yaml
- Replace initialization code
- Convert task registration calls
- Update callback structure
- Replace cancel operations
- Add
@pragmaannotations to Dart callbacks
-
Optimization
- Identify tasks that can use native workers
- Convert I/O tasks to native workers
- Add
autoDisposeto remaining Dart workers - Consider task chains for complex workflows
-
Testing
- Test all tasks in debug mode
- Test background execution (kill app)
- Test constraints (Wi-Fi, charging, battery)
- Test retry logic (simulate failures)
- Profile memory usage (before/after)
- Test on low-end Android device
- Test on iOS (30-second limit)
-
Deployment
- Gradual rollout (10% → 50% → 100%)
- Monitor crash rates
- Monitor memory metrics
- Monitor battery complaints
- Collect user feedback
-
Post-Migration
- Remove workmanager dependency
- Update documentation
- Train team on new APIs
- Celebrate improved performance! 🎉
Symptoms: enqueue() succeeds but tasks never run.
Solutions:
- Check task ID uniqueness (duplicate IDs cancel previous)
- Verify constraints aren't too restrictive
- Check Android battery optimization settings
- Enable verbose logging:
await NativeWorkManager.initialize(debugMode: true);
Symptoms: "Callback not found" error at runtime.
Solutions:
- Verify callback ID matches map key:
dartWorkers: { 'myTask': _myTaskCallback, // Key must match callbackId } DartWorker(callbackId: 'myTask') // Must match key above
- Add
@pragma('vm:entry-point')annotation - Ensure callback is top-level or static function
Symptoms: Memory not improving after migration.
Solutions:
- Verify you're using native workers (not Dart workers)
- Enable
autoDispose: truefor Dart workers - Check for memory leaks in callbacks
- Use profiler to identify actual source
Symptoms: Works on Android, fails on iOS.
Solutions:
- Check 30-second execution limit - split long tasks
- Verify Info.plist permissions
- Enable background modes in Xcode
- Test with iOS-specific constraints
A: Yes! You can run both workmanager and native_workmanager side-by-side:
dependencies:
workmanager: ^0.5.0
native_workmanager: ^1.2.2Migrate tasks one at a time, then remove workmanager when done.
A: Implement manual grouping (see "Task Tags Workaround" above) currently not supported natively.
A: No, if you follow the testing checklist. The APIs are similar enough that migration is low-risk. Test thoroughly before deploying.
A: Typical app: 30-60 minutes
- 10 minutes: Update dependencies and initialization
- 20 minutes: Convert task registration calls
- 30 minutes: Testing and verification
Large apps (50+ tasks): 2-4 hours
A: Report on GitHub Issues: https://github.com/brewkits/native_workmanager/issues
Include:
- workmanager code (before)
- native_workmanager code (after)
- Error messages or unexpected behavior
- Android/iOS version
Need assistance with migration?
- 💬 Discord Community - Ask questions, get help
- 📧 GitHub Discussions - Community support
- 🎯 Early Adopter Program - Priority migration support
🎉 Congratulations on migrating to native_workmanager! Enjoy 10x better performance!