LCOV - code coverage report
Current view: top level - src/workers - dart_worker.dart Coverage Total Hit
Test: lcov.info Lines: 100.0 % 27 27
Test Date: 2026-04-30 18:23:23 Functions: - 0 0

            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              : }
        

Generated by: LCOV version 2.4-0