[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-download-e-upload-in-background-in-flutter-con-background-downloader":3,"comments-article-download-e-upload-in-background-in-flutter-con-background-downloader":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},120,"Download e upload in background in Flutter con background_downloader","download-e-upload-in-background-in-flutter-con-background-downloader","Scaricare file di grandi dimensioni senza bloccare l'app, con progresso, notifiche native, pausa\u002Fripresa e persistenza anche quando l'app è chiusa: guida pratica al pacchetto background_downloader su Android e iOS.","## Perché non basta Dio (o http)\n\nScaricare un file con `Dio` e `ReceiveProgress` funziona benissimo… finché l'utente resta nell'app. Nel momento in cui mette l'app in background, cambia schermata o il sistema operativo decide di sospendere il processo, il download si interrompe e bisogna ricominciare da capo. Per un PDF da 200 KB è irrilevante, per un corso video da 800 MB o per l'upload di un video registrato dall'utente è un problema serio.\n\nLa soluzione corretta è delegare il trasferimento al sistema operativo: `WorkManager`\u002F`DownloadManager` su Android e `URLSession` con configurazione background su iOS. È esattamente quello che fa [`background_downloader`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fbackground_downloader): un'unica API Dart che incapsula i meccanismi nativi, con progresso, notifiche, retry, pausa e ripresa.\n\n## Installazione e configurazione\n\n```yaml\ndependencies:\n  background_downloader: ^9.2.0\n```\n\nSu Android il plugin registra da solo il servizio e il permesso `POST_NOTIFICATIONS`; su iOS non serve alcuna capability aggiuntiva per i trasferimenti (URLSession background è disponibile di default), ma se vuoi mostrare notifiche devi chiedere il permesso a runtime.\n\nNel `main()` conviene inizializzare il downloader prima di avviare l'app:\n\n```dart\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  \u002F\u002F Persiste i task in un database locale, così sopravvivono ai riavvii\n  await FileDownloader().trackTasks();\n\n  \u002F\u002F Recupera gli update generati mentre l'app era terminata\n  await FileDownloader().resumeFromBackground();\n\n  await FileDownloader().configure(\n    globalConfig: (Config.requestTimeout, const Duration(seconds: 60)),\n    androidConfig: (Config.useCacheDir, Config.whenAble),\n  );\n\n  runApp(const MyApp());\n}\n```\n\n## Il primo download\n\nUn trasferimento è descritto da un oggetto `DownloadTask` immutabile:\n\n```dart\nfinal task = DownloadTask(\n  url: 'https:\u002F\u002Fcdn.example.com\u002Fcorsi\u002Fflutter-avanzato.mp4',\n  filename: 'flutter-avanzato.mp4',\n  directory: 'corsi',\n  baseDirectory: BaseDirectory.applicationDocuments,\n  updates: Updates.statusAndProgress,\n  requiresWiFi: true,\n  retries: 3,\n  allowPause: true,\n  metaData: 'corso-42',\n);\n```\n\nAlcuni parametri meritano attenzione:\n\n- **`baseDirectory` + `directory`**: il pacchetto salva la posizione in modo *relativo*. È fondamentale su iOS, dove il path assoluto del container dell'app cambia a ogni aggiornamento: memorizzare `\u002Fvar\u002Fmobile\u002F...\u002FDocuments\u002Ffile.mp4` nel tuo database porta a file \"spariti\" dopo un update dallo store.\n- **`requiresWiFi`**: evita che l'utente consumi il piano dati; su Android è una constraint nativa del job.\n- **`retries`**: ritenta automaticamente con backoff esponenziale in caso di errore di rete.\n- **`allowPause`**: abilita pausa e ripresa, ma richiede che il server supporti le richieste HTTP `Range`.\n- **`metaData`**: campo libero per collegare il task alla tua entità di dominio (id del corso, dell'ordine, ecc.).\n\n### Modalità \"attesa\" (semplice)\n\n```dart\nfinal result = await FileDownloader().download(\n  task,\n  onProgress: (progress) => debugPrint('Progresso: ${(progress * 100).round()}%'),\n  onStatus: (status) => debugPrint('Stato: $status'),\n);\n\nswitch (result.status) {\n  case TaskStatus.complete:\n    final path = await task.filePath();\n    debugPrint('Salvato in $path');\n  case TaskStatus.canceled:\n    debugPrint('Annullato dall\\'utente');\n  case TaskStatus.failed:\n    debugPrint('Errore: ${result.exception?.description}');\n  default:\n    break;\n}\n```\n\nQuesta forma è comoda per download brevi, ma il `Future` si risolve solo finché l'app vive. Per i file pesanti usa la modalità a coda.\n\n### Modalità a coda (consigliata)\n\n```dart\nfinal accepted = await FileDownloader().enqueue(task);\n```\n\n`enqueue` restituisce immediatamente `true` se il sistema ha accettato il task: da quel momento il trasferimento è gestito dal SO e prosegue anche se l'utente chiude la schermata.\n\nGli aggiornamenti arrivano su uno stream globale, perfetto da collegare a un repository o a un notifier:\n\n```dart\nclass DownloadRepository {\n  final _progress = \u003CString, double>{};\n  final _controller = StreamController\u003CMap\u003CString, double>>.broadcast();\n\n  Stream\u003CMap\u003CString, double>> get progressStream => _controller.stream;\n\n  void listen() {\n    FileDownloader().updates.listen((update) {\n      switch (update) {\n        case TaskStatusUpdate(:final task, :final status):\n          if (status.isFinalState) {\n            _progress.remove(task.taskId);\n            _controller.add(Map.of(_progress));\n          }\n        case TaskProgressUpdate(:final task, :final progress):\n          _progress[task.taskId] = progress;\n          _controller.add(Map.of(_progress));\n      }\n    });\n  }\n}\n```\n\nNota il pattern matching di Dart 3 su `TaskUpdate`: è una gerarchia sealed, quindi lo `switch` è esaustivo senza `default`.\n\nAttenzione ai valori negativi di `progress`, che non sono percentuali ma stati speciali: `-1` fallito, `-2` annullato, `-3` non trovato, `-4` in attesa di retry, `-5` in pausa. Filtra sempre con `progress >= 0` prima di passarlo a un `LinearProgressIndicator`.\n\n## Notifiche native\n\nSu entrambe le piattaforme puoi mostrare una notifica di sistema con barra di avanzamento e pulsanti di pausa\u002Fannulla:\n\n```dart\nawait FileDownloader().permissions.request(PermissionType.notifications);\n\nFileDownloader().configureNotification(\n  running: const TaskNotification('Download in corso', '{filename} - {progress}'),\n  complete: const TaskNotification('Download completato', '{filename}'),\n  error: const TaskNotification('Download fallito', '{filename}'),\n  paused: const TaskNotification('Download in pausa', '{filename}'),\n  progressBar: true,\n  tapOpensFile: false,\n);\n```\n\nI placeholder disponibili sono `{filename}`, `{progress}`, `{networkSpeed}`, `{timeRemaining}` e `{metadata}`. Puoi anche intercettare il tap sulla notifica per portare l'utente nella schermata giusta:\n\n```dart\nFileDownloader().registerCallbacks(\n  taskNotificationTapCallback: (task, type) {\n    router.go('\u002Fcorsi\u002F${task.metaData}');\n  },\n);\n```\n\n## Pausa, ripresa e cancellazione\n\n```dart\n\u002F\u002F Richiede allowPause: true e supporto server per Range requests\nawait FileDownloader().pause(task);\nawait FileDownloader().resume(task);\n\n\u002F\u002F Cancellazione (anche di task avviati in una sessione precedente)\nawait FileDownloader().cancelTaskWithId(task.taskId);\n\n\u002F\u002F Stato corrente di un task in coda\nfinal status = await FileDownloader().taskForId(task.taskId);\n```\n\n## Persistenza dei task tra riavvii\n\nCon `trackTasks()` attivo, ogni task e il suo ultimo stato vengono salvati in un database locale. È il modo più pulito per ricostruire una schermata \"I miei download\" al riavvio dell'app:\n\n```dart\nfinal records = await FileDownloader().database.allRecords();\n\nfor (final record in records) {\n  debugPrint('${record.task.filename}: '\n      '${record.status} (${(record.progress * 100).round()}%)');\n}\n\n\u002F\u002F Pulizia dei record completati\nawait FileDownloader().database.deleteRecordWithId(someTaskId);\n```\n\nPuoi anche fornire un `PersistentStorage` personalizzato (ad esempio basato su SQLite) se vuoi integrarlo con il resto della tua persistenza.\n\n## Upload di file\n\nL'API è speculare: cambia solo la classe del task.\n\n```dart\nfinal upload = UploadTask(\n  url: 'https:\u002F\u002Fapi.example.com\u002Fv1\u002Fdocumenti',\n  filename: 'contratto.pdf',\n  directory: 'documenti',\n  baseDirectory: BaseDirectory.applicationDocuments,\n  fileField: 'file',\n  fields: {'userId': '42', 'tipo': 'contratto'},\n  headers: {'Authorization': 'Bearer $token'},\n  updates: Updates.statusAndProgress,\n);\n\nawait FileDownloader().enqueue(upload);\n```\n\nDi default viene usato un `multipart\u002Fform-data`. Se il backend si aspetta il contenuto grezzo nel body basta impostare `post: 'binary'`. Per upload multipli c'è `MultiUploadTask`, che accetta una lista di file con i rispettivi field name.\n\nUn dettaglio operativo: gli header (e quindi il token di autenticazione) vengono \"congelati\" al momento dell'`enqueue`. Se il task resta in coda a lungo e il token scade, l'upload fallisce con 401. Soluzioni tipiche: accorciare la finestra accodando solo quando necessario, oppure usare `FileDownloader().registerCallbacks(taskStatusCallback: ...)` per rilevare il 401 (leggibile in `responseStatusCode`) e ri-accodare il task con un token fresco.\n\n## Operazioni in batch\n\nPer scaricare un intero pacchetto di risorse (ad esempio i materiali di un corso) esiste `downloadBatch`:\n\n```dart\nfinal tasks = urls.map((u) => DownloadTask(url: u, directory: 'corso-42')).toList();\n\nfinal batch = await FileDownloader().downloadBatch(\n  tasks,\n  batchProgressCallback: (succeeded, failed) {\n    debugPrint('$succeeded\u002F${tasks.length} completati, $failed falliti');\n  },\n);\n\nif (batch.numFailed > 0) {\n  for (final t in batch.failed) {\n    debugPrint('Fallito: ${t.url}');\n  }\n}\n```\n\n## Esportare il file in uno spazio condiviso\n\nLa directory dell'app non è visibile all'utente. Per spostare un file scaricato nella cartella Download o in Galleria:\n\n```dart\nfinal newPath = await FileDownloader().moveToSharedStorage(\n  task,\n  SharedStorage.downloads,\n);\n```\n\nSu Android recenti l'operazione usa MediaStore e non richiede permessi di storage; su iOS il concetto di \"shared storage\" è più limitato, quindi per condividere un file è spesso preferibile il classico share sheet.\n\n## Limiti e accortezze pratiche\n\n- **iOS non garantisce l'immediatezza**: con URLSession in background il sistema decide quando eseguire i trasferimenti in base a batteria e condizioni di rete. Non progettare flussi che richiedono il completamento entro X secondi con app chiusa.\n- **Se l'utente killa l'app su iOS**, i trasferimenti in corso vengono terminati dal sistema: al riavvio usa `resumeFromBackground()` e il database per ricostruire lo stato e ri-accodare ciò che serve.\n- **Non salvare path assoluti**: conserva `baseDirectory` + `directory` + `filename` e ricalcola il path con `task.filePath()`.\n- **Verifica l'integrità**: per file di grandi dimensioni confronta un checksum o almeno la dimensione attesa prima di considerare il download valido.\n- **Non abusare della coda**: centinaia di task simultanei degradano le performance; usa `Config.holdingQueue` per limitare la concorrenza (`await FileDownloader().configure(globalConfig: (Config.holdingQueue, (3, null, null)));`).\n- **Test su device reale**: l'emulatore Android e il simulatore iOS non riproducono fedelmente le politiche di background del sistema.\n\n## Quando usarlo e quando no\n\nUsa `background_downloader` quando i file sono pesanti, quando il trasferimento deve sopravvivere al background o quando vuoi notifiche di sistema \"gratis\". Per chiamate API brevi, JSON e immagini di pochi KB continua a usare Dio o `http`: l'overhead di accodare un task nativo non ha senso.\n\nRispetto a `flutter_downloader`, oggi meno mantenuto e solo per i download, questo pacchetto copre anche gli upload, ha un'API moderna basata su stream e sealed class, e una gestione della persistenza integrata: nel 2024-2025 è la scelta di riferimento per i trasferimenti file in Flutter.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F755722d4-6a80-46df-825d-ef4c4b57e908.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1729860648569-3710c1118e0b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTEzNDU3MDl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"appshunter.io","https:\u002F\u002Funsplash.com\u002F@appshunter","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-cell-phone-sitting-on-top-of-a-wooden-table-kH8_s_sugss",null,"published","2026-10-07T04:01:49+00:00","Download in background in Flutter con background_downloader","Guida pratica a background_downloader: scaricare e caricare file in background in Flutter con progresso, notifiche, pausa e ripresa su Android e iOS.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316906803]