Line data Source code
1 : import 'dart:convert';
2 : import 'package:flutter/foundation.dart';
3 : import '../worker.dart';
4 :
5 : /// Marks a top-level function as a [DartWorker] callback.
6 : ///
7 : /// Use with `native_workmanager_gen` to auto-generate:
8 : /// - A type-safe `WorkerIds` constants class (eliminates magic strings).
9 : /// - A `generatedWorkerRegistry` map ready for [NativeWorkManager.initialize].
10 : ///
11 : /// ## Setup
12 : ///
13 : /// 1. Add `native_workmanager_gen` to `dev_dependencies` in pubspec.yaml.
14 : /// 2. Annotate top-level functions and add a `part` directive:
15 : ///
16 : /// ```dart
17 : /// // lib/workers.dart
18 : /// import 'package:native_workmanager/native_workmanager.dart';
19 : ///
20 : /// part 'workers.g.dart';
21 : ///
22 : /// @WorkerCallback('sync_contacts')
23 : /// Future<bool> syncContacts(Map<String, dynamic>? input) async {
24 : /// // ...
25 : /// return true;
26 : /// }
27 : ///
28 : /// @WorkerCallback('backup_photos')
29 : /// Future<bool> backupPhotos(Map<String, dynamic>? input) async {
30 : /// // ...
31 : /// return true;
32 : /// }
33 : /// ```
34 : ///
35 : /// 3. Run code generation:
36 : /// ```sh
37 : /// dart run build_runner build
38 : /// ```
39 : ///
40 : /// 4. Use generated code:
41 : /// ```dart
42 : /// // In main.dart
43 : /// await NativeWorkManager.initialize(
44 : /// dartWorkers: generatedWorkerRegistry,
45 : /// );
46 : ///
47 : /// // Schedule with compile-time-safe ID:
48 : /// DartWorker(callbackId: WorkerIds.syncContacts)
49 : /// ```
50 : ///
51 : /// ## Constraints
52 : ///
53 : /// - Must be applied to a **top-level function** (not a method or closure).
54 : /// - Function must have the signature `Future<bool> Function(Map<String, dynamic>?)`.
55 : /// - The `id` must be **unique** across all annotated functions in the same library.
56 :
57 : /// Callback type for Dart workers.
58 : ///
59 : /// [input] - JSON-decoded input data passed when scheduling.
60 : /// Returns `true` for success, `false` for failure.
61 : typedef DartWorkerCallback = Future<bool> Function(Map<String, dynamic>? input);
62 :
63 : /// Dart callback worker for custom logic (requires Flutter Engine).
64 : ///
65 : /// Executes Dart code in a background isolate. This starts the Flutter Engine,
66 : /// which uses more resources (~50MB RAM) but gives you full access to Dart/Flutter
67 : /// APIs, packages, and local databases.
68 : ///
69 : /// ---
70 : /// ## ⚠️ WARNING — Resource Cost
71 : ///
72 : /// Every `DartWorker` task **boots a Flutter Engine** in the background:
73 : ///
74 : /// | Resource | DartWorker | NativeWorker |
75 : /// |----------|------------|--------------|
76 : /// | RAM | ~50 MB | ~2 MB |
77 : /// | CPU startup | ~500–1000 ms cold / ~100 ms warm | < 100 ms |
78 : /// | Battery | High (JIT/AOT warm-up) | Low |
79 : ///
80 : /// **Practical limits:**
81 : /// - Running 3+ DartWorker tasks concurrently can push total background RAM
82 : /// above 150 MB, risking OS termination on low-memory devices.
83 : /// - On iOS, background execution time is strictly budgeted (~30 s for
84 : /// `BGAppRefreshTask`, ~minutes for `BGProcessingTask`). Engine startup
85 : /// alone can consume a significant portion of that budget.
86 : ///
87 : /// **Use [NativeWorker] instead whenever possible.** Only reach for
88 : /// `DartWorker` when you genuinely need Dart/Flutter APIs (e.g. sqflite,
89 : /// Hive, custom Dart packages) in the background.
90 : ///
91 : /// **Set `autoDispose: true`** on infrequent tasks to free the ~50 MB RAM
92 : /// immediately after the callback returns, at the cost of a cold-start
93 : /// penalty on the next execution.
94 : /// ---
95 : ///
96 : /// **Resource Cost:** Starts Flutter Engine (~50MB RAM vs ~2MB for NativeWorker)
97 : /// **Flexibility:** Full Dart/Flutter API access
98 : /// **Use Case:** Complex logic, database access, response processing
99 : ///
100 : /// ## Complete Example - Process API Response
101 : ///
102 : /// ```dart
103 : /// // 1. Register callback during initialization
104 : /// void main() async {
105 : /// WidgetsFlutterBinding.ensureInitialized();
106 : ///
107 : /// await NativeWorkManager.initialize(
108 : /// dartWorkers: {
109 : /// 'processSync': (input) async {
110 : /// // Make HTTP call
111 : /// final response = await http.get(
112 : /// Uri.parse('https://api.example.com/sync'),
113 : /// );
114 : ///
115 : /// // Parse JSON response
116 : /// final data = jsonDecode(response.body);
117 : ///
118 : /// // Save to local database
119 : /// final db = await openDatabase('app.db');
120 : /// for (var item in data['items']) {
121 : /// await db.insert('items', item);
122 : /// }
123 : ///
124 : /// return true; // Success
125 : /// },
126 : /// },
127 : /// );
128 : ///
129 : /// runApp(MyApp());
130 : /// }
131 : ///
132 : /// // 2. Schedule the worker
133 : /// await NativeWorkManager.enqueue(
134 : /// taskId: 'sync-with-processing',
135 : /// trigger: TaskTrigger.periodic(Duration(hours: 6)),
136 : /// worker: DartWorker(callbackId: 'processSync'),
137 : /// constraints: Constraints.networkRequired,
138 : /// );
139 : /// ```
140 : ///
141 : /// ## Example - Database Cleanup
142 : ///
143 : /// ```dart
144 : /// await NativeWorkManager.initialize(
145 : /// dartWorkers: {
146 : /// 'cleanupDatabase': (input) async {
147 : /// final db = await openDatabase('app.db');
148 : ///
149 : /// // Delete old records
150 : /// await db.delete(
151 : /// 'cache',
152 : /// where: 'timestamp < ?',
153 : /// whereArgs: [DateTime.now().subtract(Duration(days: 7)).millisecondsSinceEpoch],
154 : /// );
155 : ///
156 : /// // Vacuum database
157 : /// await db.execute('VACUUM');
158 : ///
159 : /// return true;
160 : /// },
161 : /// },
162 : /// );
163 : ///
164 : /// await NativeWorkManager.enqueue(
165 : /// taskId: 'daily-cleanup',
166 : /// trigger: TaskTrigger.periodic(Duration(days: 1)),
167 : /// worker: DartWorker(callbackId: 'cleanupDatabase'),
168 : /// );
169 : /// ```
170 : ///
171 : /// ## Example - Image Processing
172 : ///
173 : /// ```dart
174 : /// await NativeWorkManager.initialize(
175 : /// dartWorkers: {
176 : /// 'processImages': (input) async {
177 : /// final imagePaths = input?['paths'] as List<String>;
178 : ///
179 : /// for (var path in imagePaths) {
180 : /// // Read image
181 : /// final image = await decodeImageFromList(
182 : /// await File(path).readAsBytes(),
183 : /// );
184 : ///
185 : /// // Resize and compress
186 : /// final resized = await FlutterImageCompress.compressWithFile(
187 : /// path,
188 : /// minWidth: 1024,
189 : /// minHeight: 1024,
190 : /// quality: 85,
191 : /// );
192 : ///
193 : /// // Save compressed version
194 : /// await File('$path.compressed').writeAsBytes(resized);
195 : /// }
196 : ///
197 : /// return true;
198 : /// },
199 : /// },
200 : /// );
201 : ///
202 : /// await NativeWorkManager.enqueue(
203 : /// taskId: 'compress-images',
204 : /// trigger: TaskTrigger.oneTime(),
205 : /// worker: DartWorker(
206 : /// callbackId: 'processImages',
207 : /// input: {
208 : /// 'paths': ['/path/img1.jpg', '/path/img2.jpg'],
209 : /// },
210 : /// ),
211 : /// );
212 : /// ```
213 : ///
214 : /// ## Constructor Parameters
215 : ///
216 : /// **[callbackId]** *(required)* - ID of registered callback.
217 : /// - Must match a key in dartWorkers map from initialize()
218 : /// - Throws `StateError` if not registered
219 : /// - Throws `ArgumentError` if empty
220 : ///
221 : /// **[input]** *(optional)* - Data to pass to callback.
222 : /// - Will be JSON encoded/decoded automatically
223 : /// - Available as parameter in callback function
224 : /// - Can be null if callback needs no input
225 : ///
226 : /// ## Callback Requirements
227 : ///
228 : /// Your callback function must:
229 : /// - Be a top-level or static function (not a closure)
230 : /// - Return `Future<bool>` (true = success, false = failure)
231 : /// - Accept optional `Map<String, dynamic>?` parameter
232 : /// - Be registered in NativeWorkManager.initialize()
233 : ///
234 : /// ```dart
235 : /// // ✅ GOOD - Top-level function
236 : /// Future<bool> myWorker(Map<String, dynamic>? input) async {
237 : /// // Your logic here
238 : /// return true;
239 : /// }
240 : ///
241 : /// // ❌ BAD - Anonymous function (won't work in background isolate)
242 : /// dartWorkers: {
243 : /// 'worker': (input) async => true, // Won't work!
244 : /// }
245 : /// ```
246 : ///
247 : /// ## When to Use DartWorker
248 : ///
249 : /// ✅ **Use DartWorker when:**
250 : /// - You need to process API responses
251 : /// - You need database access (sqflite, hive, etc.)
252 : /// - You need complex Dart logic or algorithms
253 : /// - You need to use Dart/Flutter packages
254 : /// - You need to transform/process data
255 : ///
256 : /// ❌ **Don't use DartWorker when:**
257 : /// - Simple HTTP request is enough → Use `NativeWorker.httpRequest`
258 : /// - Just uploading/downloading files → Use `NativeWorker.httpUpload/Download`
259 : /// - Fire-and-forget JSON API call → Use `NativeWorker.httpSync`
260 : ///
261 : /// ## Performance Comparison
262 : ///
263 : /// | Aspect | DartWorker | NativeWorker |
264 : /// |--------|------------|--------------|
265 : /// | RAM Usage | ~50MB | ~2MB |
266 : /// | Startup Time | ~2-3 seconds | <100ms |
267 : /// | Capabilities | Full Dart/Flutter | HTTP only |
268 : /// | Use Case | Complex logic | Simple HTTP |
269 : ///
270 : /// ## Common Pitfalls
271 : ///
272 : /// ❌ **Don't** use anonymous functions (must be top-level/static)
273 : /// ❌ **Don't** forget to register callback in initialize()
274 : /// ❌ **Don't** use DartWorker for simple HTTP (wasteful)
275 : /// ❌ **Don't** access UI/BuildContext (background isolate)
276 : /// ✅ **Do** use for complex processing
277 : /// ✅ **Do** return true/false from callback
278 : /// ✅ **Do** handle errors gracefully in callback
279 : /// ✅ **Do** keep callbacks focused and efficient
280 : ///
281 : /// ## Error Handling
282 : ///
283 : /// ```dart
284 : /// dartWorkers: {
285 : /// 'safeWorker': (input) async {
286 : /// try {
287 : /// // Your logic
288 : /// await riskyOperation();
289 : /// return true;
290 : /// } catch (e) {
291 : /// print('Worker error: $e');
292 : /// return false; // Mark as failed
293 : /// }
294 : /// },
295 : /// }
296 : /// ```
297 : ///
298 : /// ## Platform Notes
299 : ///
300 : /// **Android:**
301 : /// - Starts Flutter Engine in WorkManager worker
302 : /// - Background isolate with full Dart VM
303 : /// - Can access SQLite, SharedPreferences, etc.
304 : ///
305 : /// **iOS:**
306 : /// - Starts Flutter Engine in BGProcessingTask
307 : /// - Background isolate with full Dart VM
308 : /// - Limited execution time (iOS may terminate)
309 : ///
310 : /// ## See Also
311 : ///
312 : /// - [NativeWorker] - Lightweight HTTP workers (no Flutter Engine)
313 : /// - [NativeWorkManager.initialize] - Register dart workers
314 : /// - [DartWorkerCallback] - Callback function type
315 : final class DartWorker extends Worker {
316 11 : DartWorker({
317 : required this.callbackId,
318 : this.input,
319 : this.autoDispose = false,
320 : this.timeoutMs,
321 : }) {
322 22 : if (callbackId.isEmpty) {
323 1 : throw ArgumentError(
324 : 'callbackId cannot be empty. '
325 : 'Use the ID you registered in NativeWorkManager.initialize().',
326 : );
327 : }
328 : // Validate input is JSON-serializable at construction time so developers
329 : // see a clear error here rather than a cryptic failure during enqueue().
330 11 : if (input != null) {
331 : try {
332 16 : jsonEncode(input);
333 1 : } on JsonUnsupportedObjectError catch (e) {
334 2 : throw ArgumentError(
335 : 'DartWorker.input must be JSON-serializable '
336 : '(String, int, double, bool, List, Map, null only).\n'
337 1 : 'Unsupported value: ${e.unsupportedObject} '
338 2 : '(${e.unsupportedObject.runtimeType})',
339 : );
340 : }
341 : }
342 : }
343 :
344 : /// ID of the registered callback (from initialize()).
345 : final String callbackId;
346 :
347 : /// Optional input data (will be JSON encoded).
348 : final Map<String, dynamic>? input;
349 :
350 : /// Maximum time in milliseconds the callback is allowed to run.
351 : ///
352 : /// If the callback does not complete within this duration, the worker will
353 : /// be killed and the task will be marked as failed.
354 : ///
355 : /// Defaults to `null`, which uses the platform default (300 000 ms / 5 min).
356 : /// Increase this for long-running tasks; decrease it to fail fast on hangs.
357 : ///
358 : /// ```dart
359 : /// // Heavy sync that may take up to 10 minutes
360 : /// DartWorker(callbackId: 'heavySync', timeoutMs: 10 * 60 * 1000)
361 : ///
362 : /// // Quick health-check — fail within 30 s if hung
363 : /// DartWorker(callbackId: 'healthCheck', timeoutMs: 30 * 1000)
364 : /// ```
365 : final int? timeoutMs;
366 :
367 : /// Whether to dispose Flutter Engine immediately after task completes.
368 : ///
369 : /// **Memory-First Mode (autoDispose: true)**:
370 : /// - Engine is killed immediately after callback returns
371 : /// - Frees ~50MB RAM instantly
372 : /// - Next task will have cold start penalty (~500ms)
373 : /// - Best for: Infrequent tasks, low-memory devices
374 : ///
375 : /// **Performance-First Mode (autoDispose: false, default)**:
376 : /// - Engine stays alive for 5 minutes
377 : /// - Next task within 5min has warm start (~100ms)
378 : /// - Uses ~50MB RAM during idle period
379 : /// - Best for: Frequent tasks, task chains
380 : ///
381 : /// Example:
382 : /// ```dart
383 : /// // One-off sync task (dispose immediately to save RAM)
384 : /// DartWorker(
385 : /// callbackId: 'syncData',
386 : /// autoDispose: true, // Kill engine after done
387 : /// )
388 : ///
389 : /// // Frequent monitoring task (keep engine warm)
390 : /// DartWorker(
391 : /// callbackId: 'checkUpdates',
392 : /// autoDispose: false, // Keep engine for 5min
393 : /// )
394 : /// ```
395 : final bool autoDispose;
396 :
397 5 : @override
398 : String get workerClassName => 'DartCallbackWorker';
399 :
400 9 : @override
401 9 : Map<String, dynamic> toMap() => {
402 9 : 'workerType': 'dartCallback',
403 18 : 'callbackId': callbackId,
404 34 : 'input': input != null ? jsonEncode(input) : null,
405 18 : 'autoDispose': autoDispose,
406 13 : if (timeoutMs != null) 'timeoutMs': timeoutMs,
407 : };
408 : }
409 :
410 : /// Internal DartWorker with callback handle.
411 : ///
412 : /// This class is used internally by NativeWorkManager to pass the callback
413 : /// handle to the native side. Users should use [DartWorker] instead.
414 : ///
415 : /// DO NOT use this class directly - it's for internal use only.
416 : @immutable
417 : final class DartWorkerInternal extends Worker {
418 4 : const DartWorkerInternal({
419 : required this.callbackId,
420 : required this.callbackHandle,
421 : this.input,
422 : this.autoDispose = false,
423 : this.timeoutMs,
424 : });
425 :
426 : /// ID of the registered callback.
427 : final String callbackId;
428 :
429 : /// Serializable callback handle for cross-isolate communication.
430 : final int callbackHandle;
431 :
432 : /// Optional input data (will be JSON encoded).
433 : final Map<String, dynamic>? input;
434 :
435 : /// Whether to dispose Flutter Engine immediately after task completes.
436 : final bool autoDispose;
437 :
438 : /// Maximum time in milliseconds the callback is allowed to run.
439 : /// `null` means use the platform default (5 min).
440 : final int? timeoutMs;
441 :
442 1 : @override
443 : String get workerClassName => 'DartCallbackWorker';
444 :
445 2 : @override
446 2 : Map<String, dynamic> toMap() => {
447 2 : 'workerType': 'dartCallback',
448 4 : 'callbackId': callbackId,
449 4 : 'callbackHandle': callbackHandle,
450 6 : 'input': input != null ? jsonEncode(input) : null,
451 4 : 'autoDispose': autoDispose,
452 2 : if (timeoutMs != null) 'timeoutMs': timeoutMs,
453 : };
454 : }
|