[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-selezione-e-upload-di-file-in-flutter-da-image-picker-al-caricamento-robusto":3,"comments-article-selezione-e-upload-di-file-in-flutter-da-image-picker-al-caricamento-robusto":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},99,"Selezione e upload di file in Flutter: da image_picker al caricamento robusto","selezione-e-upload-di-file-in-flutter-da-image-picker-al-caricamento-robusto","Come far scegliere all'utente foto e documenti con image_picker e file_picker, comprimere e ritagliare le immagini e caricarle sul server con Dio gestendo progresso, annullamento e retry.","Quasi ogni app reale, prima o poi, chiede all'utente di selezionare qualcosa: la foto del profilo, un allegato PDF, una galleria di immagini per un annuncio. Sembra un'operazione banale, ma tra permessi di sistema, differenze tra Android e iOS, file da 12 MP scattati con la fotocamera e connessioni instabili, è uno dei punti in cui le app si rompono più spesso.\n\nIn questa guida costruiamo una pipeline completa: **selezione → normalizzazione (crop + compressione) → upload con progresso e annullamento**, tenendo conto anche del Web.\n\n## Le dipendenze\n\n```yaml\ndependencies:\n  image_picker: ^1.1.2        # foto e video da galleria\u002Ffotocamera\n  file_picker: ^8.1.2         # file generici (PDF, zip, csv...)\n  flutter_image_compress: ^2.3.0\n  image_cropper: ^8.0.2\n  dio: ^5.7.0\n  http_parser: ^4.0.2         # per il contentType del multipart\n  path_provider: ^2.1.4\n```\n\nRegola generale: **`image_picker` per le immagini, `file_picker` per tutto il resto**. Il primo si integra con i selettori nativi di foto (che su Android 13+ e iOS 14+ non richiedono permessi), il secondo apre il document picker di sistema.\n\n## Configurazione nativa minima\n\nSu **iOS**, in `ios\u002FRunner\u002FInfo.plist` servono le descrizioni d'uso, altrimenti l'app viene terminata dal sistema:\n\n```xml\n\u003Ckey>NSPhotoLibraryUsageDescription\u003C\u002Fkey>\n\u003Cstring>Serve per selezionare le immagini da allegare.\u003C\u002Fstring>\n\u003Ckey>NSCameraUsageDescription\u003C\u002Fkey>\n\u003Cstring>Serve per scattare foto da allegare.\u003C\u002Fstring>\n\u003Ckey>NSMicrophoneUsageDescription\u003C\u002Fkey>\n\u003Cstring>Serve per registrare l'audio dei video.\u003C\u002Fstring>\n```\n\nSu **Android** conviene attivare esplicitamente il Photo Picker di sistema, che evita del tutto la richiesta dei permessi di storage:\n\n```dart\nimport 'package:image_picker_android\u002Fimage_picker_android.dart';\nimport 'package:image_picker_platform_interface\u002Fimage_picker_platform_interface.dart';\n\nvoid configurePicker() {\n  final platform = ImagePickerPlatform.instance;\n  if (platform is ImagePickerAndroid) {\n    platform.useAndroidPhotoPicker = true;\n  }\n}\n```\n\nChiama `configurePicker()` una volta in `main()`. Se hai già `permission_handler` nel progetto, ricorda che per la galleria su Android moderno **non serve più** chiedere `READ_EXTERNAL_STORAGE`: usare il Photo Picker è la strada consigliata da Google (ed evita rifiuti in review).\n\n## Selezionare immagini\n\nL'API di `image_picker` restituisce `XFile`, un'astrazione che funziona anche sul Web (dove il concetto di `path` non esiste).\n\n```dart\nfinal _picker = ImagePicker();\n\nFuture\u003CXFile?> pickPhoto({required bool fromCamera}) {\n  return _picker.pickImage(\n    source: fromCamera ? ImageSource.camera : ImageSource.gallery,\n    \u002F\u002F Ridimensionamento lato plugin: primo livello di difesa\n    maxWidth: 2048,\n    maxHeight: 2048,\n    imageQuality: 88,\n    preferredCameraDevice: CameraDevice.rear,\n  );\n}\n\nFuture\u003CList\u003CXFile>> pickPhotos() {\n  return _picker.pickMultiImage(maxWidth: 2048, imageQuality: 88, limit: 10);\n}\n```\n\nI parametri `maxWidth`\u002F`imageQuality` sono la cosa più economica che puoi fare per non trascinarti dietro JPEG da 8 MB. Attenzione però: se ti serve l'immagine originale (per esempio per un'elaborazione OCR), non impostarli.\n\n### Il caso dimenticato: la lost data su Android\n\nQuando l'utente apre la fotocamera, Android può terminare la tua activity per liberare memoria. Al rientro, la foto esiste ma il `Future` della chiamata è morto insieme al processo. Va recuperata a mano:\n\n```dart\nFuture\u003Cvoid> recoverLostPhoto() async {\n  final response = await _picker.retrieveLostData();\n  if (response.isEmpty) return;\n\n  final files = response.files;\n  if (files != null && files.isNotEmpty) {\n    \u002F\u002F reinserisci i file nello stato dell'app\n    _onPicked(files);\n  } else if (response.exception != null) {\n    debugPrint('Recupero fallito: ${response.exception!.code}');\n  }\n}\n```\n\nChiamala nell'`initState` della schermata di upload (o all'avvio dell'app). È due righe di codice che eliminano una classe intera di bug report irriproducibili.\n\n## Selezionare file generici\n\n```dart\nFuture\u003Cvoid> pickDocuments() async {\n  final result = await FilePicker.platform.pickFiles(\n    type: FileType.custom,\n    allowedExtensions: const ['pdf', 'docx', 'csv'],\n    allowMultiple: true,\n    withData: false,      \u002F\u002F niente bytes in RAM su mobile\n    withReadStream: true, \u002F\u002F utile per file grandi\n  );\n\n  if (result == null) return; \u002F\u002F l'utente ha annullato\n\n  for (final file in result.files) {\n    debugPrint('${file.name} — ${file.size} byte — ${file.extension}');\n  }\n}\n```\n\nTre avvertenze importanti:\n\n- `withData: true` carica **tutto** il file in memoria: su un video da 500 MB è un crash garantito. Su mobile usa `path` o `readStream`.\n- Su **Web** `path` è sempre `null`: lì devi usare `bytes` o `readStream`.\n- `allowedExtensions` funziona solo con `FileType.custom`; su iOS il filtro si basa sugli UTI, quindi estensioni esotiche possono non comparire.\n\n## Normalizzare: crop e compressione\n\nPrima di caricare, conviene portare i file in un formato prevedibile. Per l'avatar quadrato, `image_cropper` apre l'editor nativo:\n\n```dart\nFuture\u003CString?> cropSquare(String sourcePath) async {\n  final cropped = await ImageCropper().cropImage(\n    sourcePath: sourcePath,\n    aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1),\n    compressFormat: ImageCompressFormat.jpg,\n    compressQuality: 90,\n    uiSettings: [\n      AndroidUiSettings(\n        toolbarTitle: 'Ritaglia',\n        lockAspectRatio: true,\n        hideBottomControls: true,\n      ),\n      IOSUiSettings(title: 'Ritaglia', aspectRatioLockEnabled: true),\n    ],\n  );\n  return cropped?.path;\n}\n```\n\nPer la compressione, `flutter_image_compress` lavora in codice nativo: non blocca la UI e non ha bisogno di un `Isolate` come farebbe il pacchetto `image` in puro Dart.\n\n```dart\nimport 'package:path\u002Fpath.dart' as p;\nimport 'package:path_provider\u002Fpath_provider.dart';\n\nFuture\u003CXFile?> compressImage(XFile input, {int maxSide = 1600}) async {\n  final dir = await getTemporaryDirectory();\n  final target = p.join(\n    dir.path,\n    '${DateTime.now().microsecondsSinceEpoch}.jpg',\n  );\n\n  return FlutterImageCompress.compressAndGetFile(\n    input.path,\n    target,\n    quality: 82,\n    minWidth: maxSide,\n    minHeight: maxSide,\n    format: CompressFormat.jpeg,\n    keepExif: false, \u002F\u002F via i metadati GPS: privacy e byte in meno\n  );\n}\n```\n\n`minWidth`\u002F`minHeight` non forzano un ridimensionamento esatto: il plugin scala mantenendo le proporzioni in modo che il lato minore non scenda sotto quei valori. `keepExif: false` è una scelta consapevole: rimuove la geolocalizzazione dello scatto, che difficilmente vuoi spedire al tuo backend senza avvisare l'utente.\n\nUsa sempre la **cartella temporanea** per i file derivati: il sistema può ripulirla e non ti ritrovi la cache dell'app gonfiata di duplicati.\n\n## Upload multipart con Dio\n\nEcco il cuore: un servizio che carica un file con progresso, annullamento e gestione degli errori tipici.\n\n```dart\nimport 'package:dio\u002Fdio.dart';\nimport 'package:http_parser\u002Fhttp_parser.dart';\n\nclass UploadService {\n  UploadService(this._dio);\n  final Dio _dio;\n\n  Future\u003CString> uploadImage({\n    required XFile file,\n    required String albumId,\n    void Function(double progress)? onProgress,\n    CancelToken? cancelToken,\n  }) async {\n    final multipart = kIsWeb\n        ? MultipartFile.fromBytes(\n            await file.readAsBytes(),\n            filename: file.name,\n            contentType: MediaType('image', 'jpeg'),\n          )\n        : await MultipartFile.fromFile(\n            file.path,\n            filename: p.basename(file.path),\n            contentType: MediaType('image', 'jpeg'),\n          );\n\n    final formData = FormData.fromMap({\n      'album_id': albumId,\n      'file': multipart,\n    });\n\n    final response = await _dio.post\u003CMap\u003CString, dynamic>>(\n      '\u002Fmedia',\n      data: formData,\n      cancelToken: cancelToken,\n      onSendProgress: (sent, total) {\n        if (total > 0) onProgress?.call(sent \u002F total);\n      },\n      options: Options(\n        \u002F\u002F l'upload può essere lento: non usare il timeout di default\n        sendTimeout: const Duration(minutes: 5),\n        receiveTimeout: const Duration(minutes: 1),\n      ),\n    );\n\n    return response.data!['url'] as String;\n  }\n}\n```\n\nPunti da non sbagliare:\n\n- **Non riutilizzare un `FormData`**: dopo un invio è consumato. Per un retry va ricostruito da zero (per questo la creazione sta dentro il metodo).\n- **`sendTimeout`** in Dio misura l'intervallo senza trasferimento dati, ma su reti lente i default sono comunque troppo aggressivi per file di qualche MB.\n- **`contentType`** esplicito: molti backend (e i validator di Laravel\u002FRails) rifiutano un `application\u002Foctet-stream`.\n\n### Annullare e mostrare il progresso\n\n```dart\nclass UploadController extends ChangeNotifier {\n  UploadController(this._service);\n  final UploadService _service;\n\n  CancelToken? _token;\n  double progress = 0;\n  String? error;\n\n  Future\u003Cvoid> start(XFile file) async {\n    _token = CancelToken();\n    progress = 0;\n    error = null;\n    notifyListeners();\n\n    try {\n      await _service.uploadImage(\n        file: file,\n        albumId: 'demo',\n        cancelToken: _token,\n        onProgress: (p) {\n          progress = p;\n          notifyListeners();\n        },\n      );\n    } on DioException catch (e) {\n      if (CancelToken.isCancel(e)) return; \u002F\u002F annullamento volontario\n      error = switch (e.type) {\n        DioExceptionType.sendTimeout ||\n        DioExceptionType.connectionTimeout =>\n          'Connessione troppo lenta, riprova.',\n        DioExceptionType.badResponse when e.response?.statusCode == 413 =>\n          'File troppo grande.',\n        _ => 'Caricamento non riuscito.',\n      };\n    } finally {\n      notifyListeners();\n    }\n  }\n\n  void cancel() => _token?.cancel('user');\n}\n```\n\nNella UI basta un `LinearProgressIndicator(value: controller.progress)` e un pulsante che chiama `cancel()`. Il `413 Payload Too Large` è l'errore più frequente in produzione: intercettarlo e suggerire una compressione più aggressiva vale più di dieci messaggi generici.\n\n### File grandi: streaming invece dei bytes\n\nPer video o archivi, non leggere mai tutto in memoria. Con `file_picker` e `withReadStream: true`:\n\n```dart\nfinal picked = result.files.single;\nfinal multipart = MultipartFile.fromStream(\n  () => picked.readStream!,\n  picked.size,\n  filename: picked.name,\n);\n```\n\nNota che uno stream è consumabile **una sola volta**: se prevedi retry, apri lo stream dal `File` (`() => File(path).openRead()`), così ogni tentativo ne crea uno nuovo.\n\n## Retry con backoff\n\nUn upload su rete mobile fallisce spesso per motivi transitori. Un piccolo wrapper risolve il 90% dei casi:\n\n```dart\nFuture\u003CT> withRetry\u003CT>(Future\u003CT> Function() action, {int attempts = 3}) async {\n  for (var i = 0; i \u003C attempts; i++) {\n    try {\n      return await action();\n    } on DioException catch (e) {\n      final retriable = e.type == DioExceptionType.connectionError ||\n          e.type == DioExceptionType.sendTimeout ||\n          (e.response?.statusCode ?? 0) >= 500;\n      if (!retriable || i == attempts - 1) rethrow;\n      await Future.delayed(Duration(seconds: 1 \u003C\u003C i)); \u002F\u002F 1s, 2s, 4s\n    }\n  }\n  throw StateError('unreachable');\n}\n```\n\nNon ritentare mai su `400`, `401`, `413` o `422`: sono errori deterministici, il retry serve solo a bruciare batteria.\n\n## Checklist finale\n\n- Attiva il **Photo Picker** su Android ed evita i permessi di storage.\n- Gestisci `retrieveLostData()` dopo l'uso della fotocamera.\n- **Comprimi e ridimensiona** prima di caricare, e rimuovi gli EXIF sensibili.\n- Scrivi i file derivati nella **directory temporanea**.\n- Imposta `sendTimeout` generoso, passa sempre un `CancelToken`, ricostruisci il `FormData` a ogni tentativo.\n- Valida dimensione ed estensione **lato client** (feedback immediato) **e lato server** (sicurezza).\n- Su Web usa `bytes`, su mobile `path` o `readStream`: incapsula la differenza in un unico punto del codice.\n\nCon questi accorgimenti la funzionalità \"allega un file\" passa dall'essere una fonte costante di ticket a un flusso prevedibile, veloce e rispettoso della batteria e del traffico dati dei tuoi utenti.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F68218407-21aa-4071-993d-099e16e5da7f.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1648984017547-448d107c9763?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODk1MzEzMDV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Prasopchok","https:\u002F\u002Funsplash.com\u002F@gotburied","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-person-taking-a-picture-with-a-cell-phone-yHpmLn33MxY",null,"published","2026-09-16T04:01:46+00:00","Upload di file e immagini in Flutter: guida pratica","Selezione con image_picker e file_picker, compressione, crop e upload multipart con Dio: progresso, annullamento e retry in un'app Flutter reale.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316910560]