[{"data":1,"prerenderedAt":79},["ShallowReactive",2],{"tutorial-upload-di-file-in-flutter-con-multipartform-data-e-dio":3,"comments-tutorial-upload-di-file-in-flutter-con-multipartform-data-e-dio":78},{"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},126,"Upload di file in Flutter con multipart\u002Fform-data e Dio","upload-di-file-in-flutter-con-multipartform-data-e-dio","Impara a caricare immagini e documenti su un server REST da Flutter: richieste multipart, progresso di upload, annullamento, retry e gestione degli errori con Dio.","Prima o poi ogni app deve inviare un file al backend: la foto del profilo, un documento, un allegato.\n\nIn questo tutorial costruiamo un servizio di upload basato su **Dio** e sul formato `multipart\u002Fform-data`, aggiungendo barra di avanzamento, annullamento della richiesta tramite `CancelToken` e una gestione degli errori leggibile.\n\nLavoreremo con un file già selezionato (ad esempio da `image_picker` o `file_picker`): qui ci concentriamo sulla parte di rete e sulla UI di feedback.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fc4504f02-af7c-4a1a-ab1b-444dfc00dfb9.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1566241477600-ac026ad43874?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTIxMjUxOTJ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Luke Chesser","https:\u002F\u002Funsplash.com\u002F@lukechesser","https:\u002F\u002Funsplash.com\u002Fphotos\u002Ftwitter-website-on-desktop-2Bdyxgz3OM0","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=xp7Xi62pgNk",{"youtube_id":17,"duration_seconds":18,"published_at":19},"xp7Xi62pgNk",122,"2026-10-16T09:24:02+00:00","intermediate",35,"3.x","published","2026-10-16T04:33:12+00:00","Upload file in Flutter con Dio e multipart\u002Fform-data","Guida pratica all'upload di file in Flutter con Dio: multipart\u002Fform-data, barra di avanzamento, CancelToken, retry e gestione degli errori.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,57,64,71],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},842,"Installare le dipendenze","Aggiungi `dio` per le richieste HTTP e `http_parser` per impostare correttamente il MIME type delle parti multipart. In questo esempio usiamo anche `image_picker` per ottenere un file da caricare.\n\nEsegui poi `flutter pub get`.","dependencies:\n  dio: ^5.4.0\n  http_parser: ^4.0.2\n  image_picker: ^1.0.7\n  path: ^1.9.0","yaml","Le dipendenze sono risolte e importabili nel progetto.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},843,2,"Creare il servizio di upload con MultipartFile","Il cuore dell'upload è `FormData`: contiene i campi testuali e uno o più `MultipartFile`.\n\nPunti chiave:\n\n- `MultipartFile.fromFile` legge il file in streaming, quindi non carica tutto in memoria;\n- `filename` è il nome che vedrà il server, non il percorso locale;\n- `contentType` evita che il backend riceva `application\u002Foctet-stream` quando si aspetta un'immagine.","import 'dart:io';\nimport 'package:dio\u002Fdio.dart';\nimport 'package:http_parser\u002Fhttp_parser.dart';\nimport 'package:path\u002Fpath.dart' as p;\n\nclass UploadService {\n  UploadService(this._dio);\n  final Dio _dio;\n\n  Future\u003CString> uploadFile({\n    required File file,\n    required String userId,\n    MediaType? mediaType,\n    void Function(int sent, int total)? onProgress,\n    CancelToken? cancelToken,\n  }) async {\n    final formData = FormData.fromMap({\n      'userId': userId,\n      'file': await MultipartFile.fromFile(\n        file.path,\n        filename: p.basename(file.path),\n        contentType: mediaType ?? MediaType('image', 'jpeg'),\n      ),\n    });\n\n    final res = await _dio.post\u003CMap\u003CString, dynamic>>(\n      '\u002Fuploads',\n      data: formData,\n      onSendProgress: onProgress,\n      cancelToken: cancelToken,\n      options: Options(sendTimeout: const Duration(minutes: 2)),\n    );\n\n    return res.data!['url'] as String;\n  }\n}","dart","Il servizio invia una richiesta POST multipart e restituisce l'URL pubblico del file.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":48,"expected_result":56,"demo_url":41,"video_url":41},844,3,"Configurare Dio e tradurre gli errori","Un upload fallisce più spesso di una GET: connessione lenta, file troppo grande (413), token scaduto (401).\n\nCentralizziamo la configurazione di Dio e trasformiamo `DioException` in un errore di dominio con messaggio comprensibile, distinguendo l'annullamento volontario dall'errore reale.","class UploadFailure implements Exception {\n  UploadFailure(this.message);\n  final String message;\n  @override\n  String toString() => message;\n}\n\nDio buildDio(String baseUrl) {\n  final dio = Dio(BaseOptions(\n    baseUrl: baseUrl,\n    connectTimeout: const Duration(seconds: 15),\n    receiveTimeout: const Duration(seconds: 30),\n  ));\n\n  dio.interceptors.add(InterceptorsWrapper(\n    onError: (e, handler) {\n      if (e.type == DioExceptionType.cancel) return handler.next(e);\n      final code = e.response?.statusCode;\n      final msg = switch (code) {\n        401 => 'Sessione scaduta, effettua di nuovo il login.',\n        413 => 'Il file supera la dimensione massima consentita.',\n        415 => 'Formato file non supportato.',\n        _ when e.type == DioExceptionType.sendTimeout =>\n          'Upload troppo lento: controlla la connessione.',\n        _ => 'Caricamento non riuscito. Riprova.',\n      };\n      handler.reject(DioException(\n        requestOptions: e.requestOptions,\n        error: UploadFailure(msg),\n        type: e.type,\n        response: e.response,\n      ));\n    },\n  ));\n\n  return dio;\n}","Ogni errore di rete arriva alla UI con un messaggio già pronto da mostrare.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":48,"expected_result":63,"demo_url":41,"video_url":41},845,4,"Validare il file prima di inviarlo","Inviare 20 MB per poi ricevere un 413 è spreco di dati e di tempo. Controlla dimensione ed estensione **prima** della richiesta e ricava il `MediaType` corretto dall'estensione.","import 'package:path\u002Fpath.dart' as p;\nimport 'package:http_parser\u002Fhttp_parser.dart';\n\nconst _maxBytes = 10 * 1024 * 1024; \u002F\u002F 10 MB\nconst _allowed = {'jpg', 'jpeg', 'png', 'pdf'};\n\nMediaType mediaTypeFor(String path) {\n  final ext = p.extension(path).replaceFirst('.', '').toLowerCase();\n  return switch (ext) {\n    'png' => MediaType('image', 'png'),\n    'jpg' || 'jpeg' => MediaType('image', 'jpeg'),\n    'pdf' => MediaType('application', 'pdf'),\n    _ => MediaType('application', 'octet-stream'),\n  };\n}\n\nFuture\u003Cvoid> validateFile(File file) async {\n  final ext = p.extension(file.path).replaceFirst('.', '').toLowerCase();\n  if (!_allowed.contains(ext)) {\n    throw UploadFailure('Estensione .$ext non ammessa.');\n  }\n  final size = await file.length();\n  if (size > _maxBytes) {\n    throw UploadFailure('Il file pesa ${(size \u002F 1048576).toStringAsFixed(1)} MB (max 10 MB).');\n  }\n}","File non validi vengono bloccati subito con un messaggio chiaro, senza traffico inutile.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":48,"expected_result":70,"demo_url":41,"video_url":41},846,5,"Costruire la UI con progresso e annullamento","Ora colleghiamo tutto a uno schermo: selezione del file, `LinearProgressIndicator` alimentato da `onSendProgress` e pulsante **Annulla** che richiama `CancelToken.cancel()`.\n\nNota: ricorda di annullare il token anche in `dispose()` se l'utente lascia la pagina durante l'upload.","class UploadPage extends StatefulWidget {\n  const UploadPage({super.key, required this.service});\n  final UploadService service;\n  @override\n  State\u003CUploadPage> createState() => _UploadPageState();\n}\n\nclass _UploadPageState extends State\u003CUploadPage> {\n  double _progress = 0;\n  bool _busy = false;\n  CancelToken? _token;\n\n  Future\u003Cvoid> _pickAndUpload() async {\n    final picked = await ImagePicker().pickImage(source: ImageSource.gallery);\n    if (picked == null) return;\n    final file = File(picked.path);\n\n    setState(() { _busy = true; _progress = 0; });\n    _token = CancelToken();\n    try {\n      await validateFile(file);\n      final url = await widget.service.uploadFile(\n        file: file,\n        userId: 'u-42',\n        mediaType: mediaTypeFor(file.path),\n        cancelToken: _token,\n        onProgress: (sent, total) {\n          if (total > 0) setState(() => _progress = sent \u002F total);\n        },\n      );\n      _show('Caricato: $url');\n    } on DioException catch (e) {\n      _show(e.type == DioExceptionType.cancel\n          ? 'Upload annullato'\n          : '${e.error ?? 'Errore di rete'}');\n    } on UploadFailure catch (e) {\n      _show(e.message);\n    } finally {\n      if (mounted) setState(() => _busy = false);\n    }\n  }\n\n  void _show(String m) {\n    if (!mounted) return;\n    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m)));\n  }\n\n  @override\n  void dispose() {\n    _token?.cancel('page disposed');\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Upload file')),\n      body: Padding(\n        padding: const EdgeInsets.all(24),\n        child: Column(\n          mainAxisAlignment: MainAxisAlignment.center,\n          children: [\n            if (_busy) ...[\n              LinearProgressIndicator(value: _progress),\n              const SizedBox(height: 8),\n              Text('${(_progress * 100).toStringAsFixed(0)}%'),\n              TextButton(\n                onPressed: () => _token?.cancel('annullato'),\n                child: const Text('Annulla'),\n              ),\n            ] else\n              ElevatedButton.icon(\n                onPressed: _pickAndUpload,\n                icon: const Icon(Icons.cloud_upload),\n                label: const Text('Scegli e carica'),\n              ),\n          ],\n        ),\n      ),\n    );\n  }\n}","Selezionando un'immagine compare la percentuale di avanzamento; il pulsante Annulla interrompe subito la richiesta.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":48,"expected_result":77,"demo_url":41,"video_url":41},847,6,"Aggiungere retry e upload multipli","Per reti instabili conviene ritentare solo gli errori transitori (timeout, 5xx), mai un 4xx. Attenzione: un `FormData` **non è riutilizzabile** dopo l'invio, va ricreato a ogni tentativo.\n\nPer più file, limita la concorrenza (2-3 upload paralleli) invece di lanciarne decine insieme.","Future\u003CT> withRetry\u003CT>(Future\u003CT> Function() action, {int max = 3}) async {\n  var attempt = 0;\n  while (true) {\n    try {\n      return await action();\n    } on DioException catch (e) {\n      final status = e.response?.statusCode ?? 0;\n      final transient = e.type == DioExceptionType.connectionTimeout ||\n          e.type == DioExceptionType.sendTimeout ||\n          e.type == DioExceptionType.connectionError ||\n          status >= 500;\n      attempt++;\n      if (!transient || attempt >= max) rethrow;\n      await Future.delayed(Duration(seconds: 1 \u003C\u003C attempt));\n    }\n  }\n}\n\n\u002F\u002F Upload sequenziale a lotti di 2\nFuture\u003CList\u003CString>> uploadAll(List\u003CFile> files, UploadService s) async {\n  final urls = \u003CString>[];\n  for (var i = 0; i \u003C files.length; i += 2) {\n    final batch = files.skip(i).take(2);\n    final res = await Future.wait(batch.map(\n      (f) => withRetry(() => s.uploadFile(file: f, userId: 'u-42')),\n    ));\n    urls.addAll(res);\n  }\n  return urls;\n}","Gli errori temporanei vengono ritentati con backoff esponenziale e più file vengono caricati in modo controllato.",[],1792316896358]