[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-eseguire-task-in-background-in-flutter-con-workmanager-sincronizzazione-periodica-affidabile":3,"comments-tutorial-eseguire-task-in-background-in-flutter-con-workmanager-sincronizzazione-periodica-affidabile":85},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},107,"Eseguire task in background in Flutter con WorkManager: sincronizzazione periodica affidabile","eseguire-task-in-background-in-flutter-con-workmanager-sincronizzazione-periodica-affidabile","Guida avanzata a workmanager: isolate di background, vincoli di esecuzione, retry con backoff esponenziale, comunicazione con la UI, configurazione BGTaskScheduler su iOS e tecniche di debug reale.","Prima o poi ogni app seria deve fare qualcosa **quando non è in primo piano**: sincronizzare ordini, caricare log, ripulire cache, scaricare contenuti. Su Android e iOS non basta un `Timer`: il processo viene ucciso, l'app viene messa in doze e il sistema decide *quando* farti girare.\n\nIl pacchetto [`workmanager`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fworkmanager) è un ponte verso le API native di scheduling: **WorkManager\u002FJobScheduler** su Android e **BGTaskScheduler \u002F Background Fetch** su iOS. Il codice Dart viene eseguito in un **isolate separato**, senza albero dei widget, senza `BuildContext`, senza i tuoi singleton già inizializzati.\n\nIn questo tutorial avanzato costruiamo una pipeline di sincronizzazione completa:\n\n- registrazione dell'entrypoint di background con `@pragma('vm:entry-point')`;\n- inizializzazione dei plugin dentro l'isolate (`DartPluginRegistrant`);\n- task one-off e periodici con **vincoli** (rete, batteria, carica) e **backoff esponenziale**;\n- logica **idempotente** con lease\u002Flock per evitare doppie esecuzioni;\n- comunicazione dei risultati alla UI (SharedPreferences + `IsolateNameServer` + notifica locale);\n- configurazione iOS e strumenti per **forzare l'esecuzione** in fase di test.\n\n> Prerequisiti: buona confidenza con async\u002Fawait, isolate e configurazione nativa Android\u002FiOS.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F380681d0-bfcb-4898-be3e-cd208807bd08.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1627807452369-a2cd0b5ca56f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA0ODM1MzN8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Mike Hindle","https:\u002F\u002Funsplash.com\u002F@mikehindle","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fbrown-metal-pipe-with-padlock-urTBnMtWWYc","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=ssNYjqmQfrs",{"youtube_id":17,"duration_seconds":18,"published_at":19},"ssNYjqmQfrs",155,"2026-09-27T10:20:03+00:00","advanced",55,"3.x (testato su 3.24)","published","2026-09-27T04:32:13+00:00","Task in background in Flutter con WorkManager","Tutorial avanzato su workmanager in Flutter: isolate di background, vincoli, retry con backoff, sincronizzazione idempotente, iOS BGTaskScheduler e debug.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,57,64,71,78],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},714,"Dipendenze e configurazione nativa","Aggiungi `workmanager` e le dipendenze che useremo nel task (HTTP, preferenze, notifiche). Poi sistema le parti native.\n\n**Android** (`android\u002Fapp\u002Fbuild.gradle`): assicurati di avere `minSdk 23` e, se usi R8, nessuna regola aggiuntiva è necessaria: il plugin registra da solo il `Worker` nativo.\n\n**iOS**: apri `ios\u002FRunner\u002FInfo.plist` e dichiara le modalità di background e gli identificatori dei task; in `AppDelegate.swift` registra l'identificatore *prima* di `GeneratedPluginRegistrant`.\n\n```swift\n\u002F\u002F ios\u002FRunner\u002FAppDelegate.swift\nimport UIKit\nimport Flutter\nimport workmanager\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    WorkmanagerPlugin.registerTask(withIdentifier: \"it.example.app.syncOrdini\")\n    UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60 * 15))\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n```\n\n> ⚠️ L'identificatore usato in `registerTask` deve coincidere **esattamente** con il nome del task che passerai a `registerPeriodicTask`\u002F`registerOneOffTask`.","# pubspec.yaml\ndependencies:\n  flutter:\n    sdk: flutter\n  workmanager: ^0.5.2\n  http: ^1.2.2\n  shared_preferences: ^2.3.2\n  flutter_local_notifications: ^17.2.3\n\n# ios\u002FRunner\u002FInfo.plist (estratto)\n# \u003Ckey>UIBackgroundModes\u003C\u002Fkey>\n# \u003Carray>\n#   \u003Cstring>fetch\u003C\u002Fstring>\n#   \u003Cstring>processing\u003C\u002Fstring>\n# \u003C\u002Farray>\n# \u003Ckey>BGTaskSchedulerPermittedIdentifiers\u003C\u002Fkey>\n# \u003Carray>\n#   \u003Cstring>it.example.app.syncOrdini\u003C\u002Fstring>\n# \u003C\u002Farray>","yaml","`flutter pub get` completa senza errori e il progetto compila su Android e iOS con le capability Background Modes attive.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},715,2,"Definire l'entrypoint di background","Il codice di background gira in un **isolate dedicato** avviato dal sistema operativo: non c'è `runApp`, non esistono i tuoi provider, i tuoi singleton o le variabili globali inizializzate in `main()`.\n\nDue accorgimenti obbligatori:\n\n1. `@pragma('vm:entry-point')` sulla funzione `callbackDispatcher`, altrimenti il tree-shaking dell'AOT la elimina e in release il task fallisce silenziosamente.\n2. Dentro l'isolate chiama `WidgetsFlutterBinding.ensureInitialized()` e `DartPluginRegistrant.ensureInitialized()`: senza il secondo, i plugin (shared_preferences, notifiche, path_provider) lanciano `MissingPluginException`.\n\nIl valore restituito da `executeTask` è il contratto con il sistema: `true` = successo, `false` = **riprova** secondo la backoff policy, `Future.error` = fallimento definitivo.","\u002F\u002F lib\u002Fbackground\u002Fworker.dart\nimport 'dart:ui';\nimport 'package:flutter\u002Fwidgets.dart';\nimport 'package:workmanager\u002Fworkmanager.dart';\n\nconst kTaskSyncOrdini = 'it.example.app.syncOrdini';\nconst kTaskUploadLog = 'it.example.app.uploadLog';\n\n@pragma('vm:entry-point')\nvoid callbackDispatcher() {\n  Workmanager().executeTask((String task, Map\u003CString, dynamic>? inputData) async {\n    WidgetsFlutterBinding.ensureInitialized();\n    DartPluginRegistrant.ensureInitialized();\n\n    try {\n      switch (task) {\n        case kTaskSyncOrdini:\n        case Workmanager.iOSBackgroundTask: \u002F\u002F fetch opportunistico iOS\n          return await SyncService().run(inputData);\n        case kTaskUploadLog:\n          return await LogUploader().run();\n        default:\n          \u002F\u002F Task sconosciuto: non ha senso ritentare.\n          return true;\n      }\n    } catch (e, st) {\n      debugPrint('[bg] errore in $task: $e\\n$st');\n      return false; \u002F\u002F il sistema ritenterà con backoff\n    }\n  });\n}\n\n\u002F\u002F lib\u002Fmain.dart\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  await Workmanager().initialize(\n    callbackDispatcher,\n    isInDebugMode: kDebugMode, \u002F\u002F notifiche di debug a ogni esecuzione\n  );\n  runApp(const MyApp());\n}","dart","In debug, ogni esecuzione del task mostra una notifica di sistema generata dal plugin con esito e durata.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":48,"expected_result":56,"demo_url":41,"video_url":41},716,3,"Registrare task one-off e periodici con vincoli e backoff","Ora pianifichiamo il lavoro. Punti chiave da tenere a mente:\n\n- **`uniqueName`** identifica l'istanza pianificata (serve per cancellarla); **`taskName`** è ciò che arriva a `executeTask`.\n- La frequenza minima reale su Android è **15 minuti**; valori inferiori vengono arrotondati.\n- `ExistingWorkPolicy.keep` evita di resettare la schedulazione a ogni avvio dell'app: usalo per i periodici, `replace` per i one-off che devono sostituire il precedente.\n- `inputData` accetta solo tipi primitivi (`String`, `int`, `double`, `bool` e liste di questi): serializza in JSON qualsiasi struttura complessa.\n- `backoffPolicy: BackoffPolicy.exponential` con delay iniziale di 30 s è un ottimo default per la rete.\n\nSu iOS `registerPeriodicTask` è supportato con un solo identificatore registrato lato nativo e l'OS decide se e quando eseguirlo: non fare affidamento sulla puntualità.","\u002F\u002F lib\u002Fbackground\u002Fscheduler.dart\nimport 'dart:convert';\nimport 'package:workmanager\u002Fworkmanager.dart';\nimport 'worker.dart';\n\nclass BackgroundScheduler {\n  static const _uniquePeriodic = 'sync-ordini-periodic';\n\n  Future\u003Cvoid> schedulePeriodicSync() {\n    return Workmanager().registerPeriodicTask(\n      _uniquePeriodic,\n      kTaskSyncOrdini,\n      frequency: const Duration(hours: 1),\n      initialDelay: const Duration(minutes: 5),\n      existingWorkPolicy: ExistingWorkPolicy.keep,\n      constraints: Constraints(\n        networkType: NetworkType.connected,\n        requiresBatteryNotLow: true,\n        requiresDeviceIdle: false,\n      ),\n      backoffPolicy: BackoffPolicy.exponential,\n      backoffPolicyDelay: const Duration(seconds: 30),\n      inputData: \u003CString, dynamic>{'source': 'periodic'},\n    );\n  }\n\n  Future\u003Cvoid> scheduleImmediateSync(List\u003CString> ordiniIds) {\n    return Workmanager().registerOneOffTask(\n      'sync-ordini-${DateTime.now().millisecondsSinceEpoch}',\n      kTaskSyncOrdini,\n      initialDelay: const Duration(seconds: 5),\n      existingWorkPolicy: ExistingWorkPolicy.append,\n      constraints: Constraints(networkType: NetworkType.connected),\n      backoffPolicy: BackoffPolicy.exponential,\n      backoffPolicyDelay: const Duration(seconds: 30),\n      inputData: \u003CString, dynamic>{\n        'source': 'manual',\n        'payload': jsonEncode({'ids': ordiniIds}),\n      },\n    );\n  }\n\n  Future\u003Cvoid> cancelAll() => Workmanager().cancelAll();\n  Future\u003Cvoid> cancelPeriodic() =>\n      Workmanager().cancelByUniqueName(_uniquePeriodic);\n}","Con `adb shell dumpsys jobscheduler | grep it.example.app` vedi il job registrato con i vincoli richiesti.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":48,"expected_result":63,"demo_url":41,"video_url":41},717,4,"Scrivere una sincronizzazione idempotente e resiliente","Il sistema può eseguire lo stesso task più volte (retry, riavvio del device, doppia schedulazione). La logica deve quindi essere **idempotente** e rispettare il budget di tempo: su Android il worker viene interrotto dopo ~10 minuti, su iOS spesso dopo ~30 secondi.\n\nStrategie applicate nel codice qui sotto:\n\n- un **lock con scadenza** su `SharedPreferences` per evitare esecuzioni concorrenti;\n- un **cursore** (`lastSyncAt`) per sincronizzare in modo incrementale;\n- un **timeout esplicito** su ogni chiamata di rete, così da chiudere ordinatamente prima che sia il sistema a ucciderci;\n- distinzione tra errori **transitori** (5xx, timeout → `false`, si ritenta) ed errori **permanenti** (4xx → `true`, inutile insistere).\n\n> Estrai sempre la logica in una classe pura e testabile: l'entrypoint deve limitarsi a fare da router.","\u002F\u002F lib\u002Fbackground\u002Fsync_service.dart\nimport 'dart:convert';\nimport 'package:http\u002Fhttp.dart' as http;\nimport 'package:shared_preferences\u002Fshared_preferences.dart';\n\nclass SyncService {\n  static const _kLock = 'sync.lock.until';\n  static const _kCursor = 'sync.lastSyncAt';\n  static const _kLastResult = 'sync.lastResult';\n\n  final http.Client _client;\n  SyncService({http.Client? client}) : _client = client ?? http.Client();\n\n  Future\u003Cbool> run(Map\u003CString, dynamic>? inputData) async {\n    final prefs = await SharedPreferences.getInstance();\n    await prefs.reload();\n\n    final now = DateTime.now().millisecondsSinceEpoch;\n    final lockUntil = prefs.getInt(_kLock) ?? 0;\n    if (now \u003C lockUntil) {\n      return true; \u002F\u002F un'altra esecuzione è già in corso\n    }\n    await prefs.setInt(_kLock, now + const Duration(minutes: 5).inMilliseconds);\n\n    try {\n      final cursor = prefs.getString(_kCursor) ?? '1970-01-01T00:00:00Z';\n      final uri = Uri.https('api.example.com', '\u002Fordini', {'since': cursor});\n\n      final res = await _client\n          .get(uri, headers: {'Accept': 'application\u002Fjson'})\n          .timeout(const Duration(seconds: 20));\n\n      if (res.statusCode >= 500) return false;            \u002F\u002F transitorio → retry\n      if (res.statusCode >= 400) {                        \u002F\u002F permanente\n        await prefs.setString(_kLastResult, 'errore ${res.statusCode}');\n        return true;\n      }\n\n      final body = jsonDecode(res.body) as Map\u003CString, dynamic>;\n      final items = (body['items'] as List).cast\u003CMap\u003CString, dynamic>>();\n      \u002F\u002F ...persisti su Hive\u002FSQLite in modo transazionale...\n\n      await prefs.setString(_kCursor, body['serverTime'] as String);\n      await prefs.setString(\n        _kLastResult,\n        '${items.length} ordini · ${DateTime.now().toIso8601String()}',\n      );\n      return true;\n    } on Exception {\n      return false; \u002F\u002F timeout \u002F socket → retry con backoff\n    } finally {\n      await prefs.remove(_kLock);\n      _client.close();\n    }\n  }\n}","Il task completa entro il budget di tempo, aggiorna il cursore e rilascia sempre il lock anche in caso di eccezione.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":48,"expected_result":70,"demo_url":41,"video_url":41},718,5,"Comunicare l'esito all'app: prefs, IsolateNameServer e notifiche","L'isolate di background **non condivide memoria** con quello della UI: non puoi aggiornare un `ChangeNotifier` direttamente. Hai tre canali complementari:\n\n1. **Storage condiviso** (`SharedPreferences`, Hive): l'UI legge al ritorno in foreground. Ricorda `prefs.reload()` lato UI, altrimenti leggi la cache in memoria e non vedi le scritture dell'altro isolate.\n2. **`IsolateNameServer` + `SendPort`**: se l'app è viva in foreground, aggiorni la UI in tempo reale.\n3. **Notifica locale**: l'unico modo per avvisare l'utente ad app chiusa.\n\nNel codice registriamo una porta nella UI e, dal background, proviamo a usarla; se non esiste, ci limitiamo allo storage (+ notifica).","\u002F\u002F lib\u002Fbackground\u002Fbg_bridge.dart\nimport 'dart:isolate';\nimport 'dart:ui';\n\nconst kSyncPortName = 'sync_port';\n\n\u002F\u002F\u002F Lato UI: apri il canale e ascolta gli aggiornamenti.\nReceivePort openSyncPort(void Function(dynamic) onMessage) {\n  IsolateNameServer.removePortNameMapping(kSyncPortName);\n  final port = ReceivePort();\n  IsolateNameServer.registerPortWithName(port.sendPort, kSyncPortName);\n  port.listen(onMessage);\n  return port;\n}\n\n\u002F\u002F\u002F Lato background: notifica l'UI se è in ascolto.\nvoid notifyUi(Map\u003CString, dynamic> payload) {\n  final send = IsolateNameServer.lookupPortByName(kSyncPortName);\n  send?.send(payload);\n}\n\n\u002F\u002F --- utilizzo nella UI ---\nclass _HomeState extends State\u003CHome> {\n  late final ReceivePort _port;\n  String _status = 'in attesa';\n\n  @override\n  void initState() {\n    super.initState();\n    _port = openSyncPort((msg) {\n      if (!mounted) return;\n      setState(() => _status = '${(msg as Map)['items']} nuovi ordini');\n    });\n  }\n\n  @override\n  void dispose() {\n    IsolateNameServer.removePortNameMapping(kSyncPortName);\n    _port.close();\n    super.dispose();\n  }\n}","Con l'app aperta la UI si aggiorna a fine sincronizzazione; ad app chiusa i dati vengono comunque persistiti e letti al rientro.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":48,"expected_result":77,"demo_url":41,"video_url":41},719,6,"Adattarsi ai vincoli di iOS","Su iOS non esiste un vero scheduler affidabile: il sistema assegna budget in base all'uso dell'app. Adegua le aspettative e il codice:\n\n- **Un solo** identificatore di task registrato lato nativo (quello dichiarato in `Info.plist`).\n- Il `frequency` è un *suggerimento*: iOS esegue quando vuole, a volte solo di notte in carica.\n- Il tempo utile è brevissimo (spesso \u003C 30 s per il fetch): progetta il task come **batch piccolo** interrompibile e riprendibile grazie al cursore.\n- `Workmanager.iOSBackgroundTask` è il nome del task ricevuto per i fetch opportunistici: gestiscilo nello switch (l'abbiamo già fatto).\n- Per testare, usa Xcode: metti un breakpoint dopo l'avvio, poi in console LLDB forza l'esecuzione con `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"it.example.app.syncOrdini\"]`.\n\nUn pattern robusto è **degradare** su iOS: schedulazione opportunistica in background + sincronizzazione forzata al ritorno in foreground tramite `AppLifecycleListener`.","\u002F\u002F lib\u002Fbackground\u002Fforeground_catchup.dart\nimport 'package:flutter\u002Fwidgets.dart';\n\nclass ForegroundCatchUp extends StatefulWidget {\n  final Widget child;\n  final Future\u003Cvoid> Function() onResume;\n  const ForegroundCatchUp({super.key, required this.child, required this.onResume});\n\n  @override\n  State\u003CForegroundCatchUp> createState() => _ForegroundCatchUpState();\n}\n\nclass _ForegroundCatchUpState extends State\u003CForegroundCatchUp> {\n  late final AppLifecycleListener _listener;\n  DateTime _last = DateTime.fromMillisecondsSinceEpoch(0);\n\n  @override\n  void initState() {\n    super.initState();\n    _listener = AppLifecycleListener(onResume: () async {\n      \u002F\u002F throttling: al massimo una sync ogni 2 minuti\n      if (DateTime.now().difference(_last) \u003C const Duration(minutes: 2)) return;\n      _last = DateTime.now();\n      await widget.onResume();\n    });\n  }\n\n  @override\n  void dispose() {\n    _listener.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) => widget.child;\n}","Su iOS la sincronizzazione avviene opportunisticamente in background e comunque entro pochi secondi dal ritorno in foreground.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":48,"expected_result":84,"demo_url":41,"video_url":41},720,7,"Debug, forzatura dell'esecuzione e test automatici","Aspettare 15 minuti per verificare un task è insostenibile. Ecco gli strumenti che uso quotidianamente.\n\n**Android — forzare un job già schedulato**\n```bash\nadb shell dumpsys jobscheduler | grep -A 3 it.example.app\n# individua il JOB #u0aXXX\u002F\u003Cid>, poi:\nadb shell cmd jobscheduler run -f it.example.app \u003Cid>\n```\n\n**Android — simulare i vincoli**\n```bash\nadb shell cmd connectivity airplane-mode enable   # niente rete → job in attesa\nadb shell dumpsys deviceidle force-idle           # doze mode\n```\n\n**Log dell'isolate**: i `debugPrint` del background compaiono in `adb logcat -s flutter`, non nella console di `flutter run` se l'app è stata chiusa.\n\n**Test**: non testare `Workmanager()`; testa la classe `SyncService` con un `http.Client` mockato e `SharedPreferences.setMockInitialValues`. Verifica i tre comportamenti critici: successo, retry su 5xx\u002Ftimeout, e idempotenza in presenza di lock.\n\n**Checklist finale di produzione**\n- `@pragma('vm:entry-point')` presente e verificato in **release**;\n- nessun accesso a `BuildContext` o a singleton della UI nell'isolate;\n- lock e cursori sempre rilasciati in `finally`;\n- `isInDebugMode: false` in release (altrimenti l'utente vede le notifiche di debug);\n- cancellazione dei task al logout (`cancelAll`).","\u002F\u002F test\u002Fsync_service_test.dart\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:http\u002Fhttp.dart' as http;\nimport 'package:http\u002Ftesting.dart';\nimport 'package:shared_preferences\u002Fshared_preferences.dart';\nimport 'package:myapp\u002Fbackground\u002Fsync_service.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() => SharedPreferences.setMockInitialValues({}));\n\n  test('successo: aggiorna il cursore e restituisce true', () async {\n    final client = MockClient((req) async => http.Response(\n          '{\"items\":[{\"id\":\"1\"}],\"serverTime\":\"2024-05-01T10:00:00Z\"}',\n          200,\n        ));\n\n    final ok = await SyncService(client: client).run(null);\n    final prefs = await SharedPreferences.getInstance();\n\n    expect(ok, isTrue);\n    expect(prefs.getString('sync.lastSyncAt'), '2024-05-01T10:00:00Z');\n    expect(prefs.getInt('sync.lock.until'), isNull); \u002F\u002F lock rilasciato\n  });\n\n  test('errore 500: richiede retry', () async {\n    final client = MockClient((req) async => http.Response('boom', 500));\n    expect(await SyncService(client: client).run(null), isFalse);\n  });\n}","I test passano con `flutter test` e il task può essere eseguito a comando dal terminale senza attendere la finestra di scheduling.",[],1792316929042]