[{"data":1,"prerenderedAt":86},["ShallowReactive",2],{"tutorial-fotocamera-in-tempo-reale-in-flutter-con-il-pacchetto-camera-anteprima-scatto-e-salvataggio":3,"comments-tutorial-fotocamera-in-tempo-reale-in-flutter-con-il-pacchetto-camera-anteprima-scatto-e-salvataggio":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},130,"Fotocamera in tempo reale in Flutter con il pacchetto camera: anteprima, scatto e salvataggio","fotocamera-in-tempo-reale-in-flutter-con-il-pacchetto-camera-anteprima-scatto-e-salvataggio","Impara a integrare la fotocamera nativa in Flutter con il pacchetto camera: permessi, anteprima live, ciclo di vita, scatto, flash, zoom e salvataggio del file su disco.","Il pacchetto [`camera`](https:\u002F\u002Fpub.dev\u002Fpackages\u002Fcamera) è la soluzione ufficiale del team Flutter per accedere al sensore fotografico del dispositivo e mostrare un'**anteprima live** direttamente nell'albero dei widget. A differenza di `image_picker`, che delega lo scatto all'app fotocamera di sistema, con `camera` costruisci la tua interfaccia: overlay personalizzati, controlli di flash e zoom, scatto con un tocco, switch tra camera frontale e posteriore.\n\nIn questo tutorial costruiremo passo dopo passo una schermata fotocamera completa e robusta. Vedremo in particolare:\n\n- la configurazione dei permessi su **Android e iOS**;\n- l'inizializzazione asincrona del `CameraController` e la gestione degli errori;\n- la corretta gestione del **ciclo di vita** (fondamentale: se non rilasci la camera quando l'app va in background rischi crash e blocchi del sensore);\n- scatto, salvataggio del file in una directory persistente e anteprima del risultato;\n- controlli avanzati: flash, zoom con pinch, messa a fuoco a tocco.\n\n> **Prerequisiti**: conoscenza di `StatefulWidget`, `Future`\u002F`async` e del pacchetto `path_provider`. Serve un **dispositivo fisico**: gli emulatori offrono una camera simulata poco affidabile, e il simulatore iOS non ha fotocamera.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Ffd824e2f-40ec-4259-87f1-f5663f1b1b5f.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1567985944845-cb4d1e598822?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTI2NDM1MDV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Josh Power","https:\u002F\u002Funsplash.com\u002F@joshwp","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fperson-holding-phone-FWoH2qhLb5I","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=bBn9p7fJfHE",{"youtube_id":17,"duration_seconds":18,"published_at":19},"bBn9p7fJfHE",151,"2026-10-22T09:47:00+00:00","intermediate",45,"3.x","published","2026-10-22T04:31:45+00:00","Fotocamera in Flutter con il pacchetto camera","Guida pratica al pacchetto camera in Flutter: permessi, anteprima live, ciclo di vita, scatto foto, flash, zoom e salvataggio su disco.",{"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},868,"Installare il pacchetto e configurare i permessi nativi","Aggiungi al `pubspec.yaml` il pacchetto `camera` e `path_provider` (per salvare gli scatti in una directory persistente dell'app):\n\n```bash\nflutter pub add camera path_provider\n```\n\nPoi configura le piattaforme native, **passaggio obbligatorio** senza il quale l'app crasha o la camera non si apre:\n\n**Android** — in `android\u002Fapp\u002Fbuild.gradle` assicurati che `minSdkVersion` sia almeno **21**:\n\n```gradle\ndefaultConfig {\n    minSdkVersion 21\n}\n```\n\nIl plugin dichiara già il permesso `CAMERA` nel proprio manifest, quindi di norma non devi aggiungerlo. Se vuoi registrare anche audio, aggiungi `RECORD_AUDIO` in `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`.\n\n**iOS** — in `ios\u002FRunner\u002FInfo.plist` servono le descrizioni d'uso, altrimenti Apple rifiuta l'app e il sistema termina il processo:\n\n```xml\n\u003Ckey>NSCameraUsageDescription\u003C\u002Fkey>\n\u003Cstring>L'app usa la fotocamera per scattare foto dei tuoi documenti.\u003C\u002Fstring>\n\u003Ckey>NSMicrophoneUsageDescription\u003C\u002Fkey>\n\u003Cstring>Il microfono serve solo per registrare video con audio.\u003C\u002Fstring>\n```\n\n> Il pacchetto `camera` richiede il permesso runtime **automaticamente** alla prima inizializzazione del controller: non serve `permission_handler`, ma devi comunque gestire il caso in cui l'utente neghi l'accesso (lo vedremo nel passo 3).","dependencies:\n  flutter:\n    sdk: flutter\n  camera: ^0.11.0\n  path_provider: ^2.1.4","yaml","Le dipendenze sono installate e i progetti nativi sono configurati: `flutter pub get` termina senza errori e l'app compila su device fisico.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},869,2,"Recuperare le camere disponibili all'avvio dell'app","La funzione `availableCameras()` interroga il sistema e restituisce la lista delle fotocamere (`CameraDescription`), ciascuna con il proprio `lensDirection` (`back`, `front`, `external`) e l'orientamento del sensore.\n\nVa chiamata **dopo** `WidgetsFlutterBinding.ensureInitialized()`, perché usa i platform channel. È buona pratica farlo una sola volta nel `main()` e passare la lista alla schermata, invece di ricalcolarla ad ogni rebuild.\n\nAttenzione a due casi reali:\n\n- la lista può essere **vuota** (dispositivi senza camera, emulatori mal configurati);\n- `availableCameras()` può lanciare una `CameraException` se i servizi camera non sono disponibili.\n\nGestiamo entrambi con un `try\u002Fcatch` e una lista di fallback vuota.","import 'package:camera\u002Fcamera.dart';\nimport 'package:flutter\u002Fmaterial.dart';\n\nList\u003CCameraDescription> cameras = \u003CCameraDescription>[];\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  try {\n    cameras = await availableCameras();\n  } on CameraException catch (e) {\n    debugPrint('Errore nel recupero delle camere: ${e.code} ${e.description}');\n  }\n  runApp(const MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      title: 'Demo Fotocamera',\n      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),\n      home: cameras.isEmpty\n          ? const Scaffold(\n              body: Center(child: Text('Nessuna fotocamera disponibile')),\n            )\n          : CameraScreen(cameras: cameras),\n    );\n  }\n}","dart","All'avvio l'app conosce l'elenco delle fotocamere del dispositivo; se non ce ne sono mostra un messaggio invece di crashare.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":48,"expected_result":56,"demo_url":41,"video_url":41},870,3,"Inizializzare il CameraController e mostrare l'anteprima","Il `CameraController` è l'oggetto che apre il sensore e alimenta il widget `CameraPreview`. L'inizializzazione è **asincrona**: finché `initialize()` non è completata, `CameraPreview` non può essere costruito.\n\nParametri chiave del costruttore:\n\n- **ResolutionPreset**: `low`, `medium`, `high`, `veryHigh`, `ultraHigh`, `max`. Più alta la risoluzione, più memoria e più lag in anteprima: `high` è un ottimo compromesso per gli scatti.\n- **enableAudio**: mettilo a `false` se scatti solo foto, così su iOS non viene richiesto il permesso microfono.\n- **imageFormatGroup**: `ImageFormatGroup.jpeg` evita conversioni costose su iOS.\n\nGestiamo gli errori con i codici tipici di `CameraException`: `CameraAccessDenied` (permesso negato), `CameraAccessRestricted` (restrizioni parentali), `AudioAccessDenied`.\n\nPer l'anteprima usiamo un `AspectRatio` basato su `controller.value.aspectRatio` in modo che l'immagine non risulti deformata.","class CameraScreen extends StatefulWidget {\n  const CameraScreen({super.key, required this.cameras});\n\n  final List\u003CCameraDescription> cameras;\n\n  @override\n  State\u003CCameraScreen> createState() => _CameraScreenState();\n}\n\nclass _CameraScreenState extends State\u003CCameraScreen> {\n  CameraController? _controller;\n  int _cameraIndex = 0;\n  String? _errorMessage;\n\n  @override\n  void initState() {\n    super.initState();\n    _setupCamera(widget.cameras[_cameraIndex]);\n  }\n\n  Future\u003Cvoid> _setupCamera(CameraDescription description) async {\n    \u002F\u002F Rilascia l'eventuale controller precedente prima di crearne uno nuovo.\n    await _controller?.dispose();\n\n    final controller = CameraController(\n      description,\n      ResolutionPreset.high,\n      enableAudio: false,\n      imageFormatGroup: ImageFormatGroup.jpeg,\n    );\n    _controller = controller;\n\n    try {\n      await controller.initialize();\n      if (!mounted) return;\n      setState(() => _errorMessage = null);\n    } on CameraException catch (e) {\n      if (!mounted) return;\n      setState(() {\n        _errorMessage = switch (e.code) {\n          'CameraAccessDenied' => 'Permesso fotocamera negato.',\n          'CameraAccessRestricted' => 'Accesso alla fotocamera limitato.',\n          _ => 'Errore fotocamera: ${e.description}',\n        };\n      });\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller?.dispose();\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final controller = _controller;\n\n    if (_errorMessage != null) {\n      return Scaffold(body: Center(child: Text(_errorMessage!)));\n    }\n    if (controller == null || !controller.value.isInitialized) {\n      return const Scaffold(\n        backgroundColor: Colors.black,\n        body: Center(child: CircularProgressIndicator()),\n      );\n    }\n\n    return Scaffold(\n      backgroundColor: Colors.black,\n      body: Center(\n        child: AspectRatio(\n          aspectRatio: controller.value.aspectRatio,\n          child: CameraPreview(controller),\n        ),\n      ),\n    );\n  }\n}","Aprendo la schermata il sistema chiede il permesso fotocamera e, una volta concesso, appare l'anteprima live a schermo con le proporzioni corrette.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":48,"expected_result":63,"demo_url":41,"video_url":41},871,4,"Gestire il ciclo di vita: rilasciare e riattivare la camera","Questo è il passo che la maggior parte dei tutorial dimentica e che causa i bug più frequenti: se l'utente mette l'app in background mentre la camera è aperta, il sistema operativo può **revocare l'accesso al sensore**. Al ritorno in foreground l'anteprima resta nera o l'app crasha.\n\nLa soluzione è usare il `WidgetsBindingObserver` e reagire a `didChangeAppLifecycleState`:\n\n- su `inactive` \u002F `paused` \u002F `hidden` → `dispose()` del controller;\n- su `resumed` → ricrea il controller con la stessa `CameraDescription`.\n\nAggiungiamo anche `onNewCameraSelected` per lo **switch** tra camera posteriore e frontale, riutilizzando `_setupCamera`.","class _CameraScreenState extends State\u003CCameraScreen>\n    with WidgetsBindingObserver {\n  \u002F\u002F ... campi del passo precedente\n\n  @override\n  void initState() {\n    super.initState();\n    WidgetsBinding.instance.addObserver(this);\n    _setupCamera(widget.cameras[_cameraIndex]);\n  }\n\n  @override\n  void dispose() {\n    WidgetsBinding.instance.removeObserver(this);\n    _controller?.dispose();\n    super.dispose();\n  }\n\n  @override\n  void didChangeAppLifecycleState(AppLifecycleState state) {\n    final controller = _controller;\n    if (controller == null || !controller.value.isInitialized) return;\n\n    switch (state) {\n      case AppLifecycleState.inactive:\n      case AppLifecycleState.paused:\n      case AppLifecycleState.hidden:\n      case AppLifecycleState.detached:\n        controller.dispose();\n        _controller = null;\n        if (mounted) setState(() {});\n      case AppLifecycleState.resumed:\n        _setupCamera(widget.cameras[_cameraIndex]);\n    }\n  }\n\n  Future\u003Cvoid> _switchCamera() async {\n    if (widget.cameras.length \u003C 2) return;\n    setState(() {\n      _cameraIndex = (_cameraIndex + 1) % widget.cameras.length;\n      _controller = null; \u002F\u002F mostra lo spinner durante la transizione\n    });\n    await _setupCamera(widget.cameras[_cameraIndex]);\n  }\n}","Mettendo l'app in background e riaprendola l'anteprima riparte correttamente; il pulsante di switch alterna camera posteriore e frontale senza errori.",{"id":65,"position":66,"title":67,"body":68,"code_snippet":69,"code_language":48,"expected_result":70,"demo_url":41,"video_url":41},872,5,"Scattare una foto e salvarla in una directory persistente","`controller.takePicture()` restituisce un `XFile` che punta a un file in una directory **temporanea**: il sistema può cancellarlo in qualsiasi momento. Se vuoi conservare lo scatto, copialo nella directory documenti dell'app ottenuta con `path_provider`.\n\nAlcune accortezze fondamentali:\n\n- evita scatti concorrenti controllando `controller.value.isTakingPicture` (o una flag `_isBusy`), altrimenti ottieni l'eccezione `captureInProgress`;\n- `takePicture()` è `async`: dopo l'`await` verifica sempre `mounted` prima di chiamare `setState` o usare il `context`;\n- genera un nome file univoco, ad esempio con il timestamp.","import 'dart:io';\nimport 'package:path_provider\u002Fpath_provider.dart';\n\nbool _isBusy = false;\nFile? _lastPicture;\n\nFuture\u003Cvoid> _takePicture() async {\n  final controller = _controller;\n  if (controller == null ||\n      !controller.value.isInitialized ||\n      controller.value.isTakingPicture ||\n      _isBusy) {\n    return;\n  }\n\n  setState(() => _isBusy = true);\n  try {\n    final XFile shot = await controller.takePicture();\n\n    final dir = await getApplicationDocumentsDirectory();\n    final fileName = 'IMG_${DateTime.now().millisecondsSinceEpoch}.jpg';\n    final saved = await File(shot.path).copy('${dir.path}\u002F$fileName');\n\n    if (!mounted) return;\n    setState(() => _lastPicture = saved);\n  } on CameraException catch (e) {\n    if (!mounted) return;\n    ScaffoldMessenger.of(context).showSnackBar(\n      SnackBar(content: Text('Scatto fallito: ${e.description}')),\n    );\n  } finally {\n    if (mounted) setState(() => _isBusy = false);\n  }\n}","Premendo il pulsante di scatto viene creato un file JPEG nella cartella documenti dell'app e il percorso viene salvato nello stato.",{"id":72,"position":73,"title":74,"body":75,"code_snippet":76,"code_language":48,"expected_result":77,"demo_url":41,"video_url":41},873,6,"Costruire i controlli: scatto, miniatura, flash e zoom con pinch","Ora componiamo l'interfaccia sopra l'anteprima usando uno `Stack`:\n\n- in basso una barra con **pulsante di scatto**, **switch camera** e **miniatura** dell'ultimo scatto;\n- in alto un pulsante per ciclare le modalità **flash** (`off`, `auto`, `always`, `torch`);\n- un `GestureDetector` che gestisce il **pinch-to-zoom** tramite `onScaleStart`\u002F`onScaleUpdate`, rispettando i limiti restituiti da `getMinZoomLevel()` e `getMaxZoomLevel()`;\n- il **tap-to-focus** con `setFocusPoint`, che accetta un `Offset` con coordinate normalizzate tra 0 e 1.\n\n> Nota: su alcuni dispositivi la camera frontale non supporta il flash né lo zoom. Avvolgi sempre queste chiamate in un `try\u002Fcatch` o verifica `controller.value.description.lensDirection`.","double _minZoom = 1.0, _maxZoom = 1.0, _zoom = 1.0, _baseZoom = 1.0;\nFlashMode _flashMode = FlashMode.off;\n\nFuture\u003Cvoid> _initZoomLimits(CameraController c) async {\n  _minZoom = await c.getMinZoomLevel();\n  _maxZoom = await c.getMaxZoomLevel();\n  _zoom = _minZoom;\n}\n\nFuture\u003Cvoid> _cycleFlash() async {\n  final controller = _controller;\n  if (controller == null) return;\n  const modes = [FlashMode.off, FlashMode.auto, FlashMode.always, FlashMode.torch];\n  final next = modes[(modes.indexOf(_flashMode) + 1) % modes.length];\n  try {\n    await controller.setFlashMode(next);\n    if (mounted) setState(() => _flashMode = next);\n  } on CameraException catch (_) {\n    \u002F\u002F flash non supportato su questa camera\n  }\n}\n\nWidget _buildPreviewWithGestures(CameraController controller) {\n  return GestureDetector(\n    onScaleStart: (_) => _baseZoom = _zoom,\n    onScaleUpdate: (details) async {\n      final newZoom = (_baseZoom * details.scale).clamp(_minZoom, _maxZoom);\n      if ((newZoom - _zoom).abs() \u003C 0.01) return;\n      _zoom = newZoom;\n      await controller.setZoomLevel(_zoom);\n    },\n    onTapUp: (details) async {\n      final box = context.findRenderObject() as RenderBox;\n      final local = box.globalToLocal(details.globalPosition);\n      final point = Offset(local.dx \u002F box.size.width, local.dy \u002F box.size.height);\n      try {\n        await controller.setFocusPoint(point);\n        await controller.setExposurePoint(point);\n      } on CameraException catch (_) {}\n    },\n    child: CameraPreview(controller),\n  );\n}\n\nWidget _buildControls() {\n  return Positioned(\n    left: 0,\n    right: 0,\n    bottom: 24,\n    child: Row(\n      mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n      children: [\n        SizedBox(\n          width: 56,\n          height: 56,\n          child: _lastPicture == null\n              ? const SizedBox.shrink()\n              : ClipRRect(\n                  borderRadius: BorderRadius.circular(8),\n                  child: Image.file(_lastPicture!, fit: BoxFit.cover),\n                ),\n        ),\n        GestureDetector(\n          onTap: _isBusy ? null : _takePicture,\n          child: Container(\n            width: 76,\n            height: 76,\n            decoration: BoxDecoration(\n              shape: BoxShape.circle,\n              color: Colors.white.withOpacity(_isBusy ? 0.4 : 1),\n              border: Border.all(color: Colors.white54, width: 4),\n            ),\n          ),\n        ),\n        IconButton(\n          iconSize: 36,\n          color: Colors.white,\n          onPressed: _switchCamera,\n          icon: const Icon(Icons.cameraswitch),\n        ),\n      ],\n    ),\n  );\n}","L'anteprima risponde al pinch per lo zoom e al tap per la messa a fuoco; la barra inferiore mostra il pulsante di scatto, lo switch camera e la miniatura dell'ultima foto.",{"id":79,"position":80,"title":81,"body":82,"code_snippet":83,"code_language":48,"expected_result":84,"demo_url":41,"video_url":41},874,7,"Aprire l'anteprima a schermo intero e rifinire la UX","Chiudiamo il flusso aggiungendo una schermata di anteprima dello scatto, dove l'utente può **confermare** o **ripetere** la foto. Il risultato viene restituito con `Navigator.pop` al chiamante.\n\nUltime rifiniture consigliate prima di andare in produzione:\n\n- **Bloccare l'orientamento** durante lo scatto con `controller.lockCaptureOrientation(DeviceOrientation.portraitUp)` se la tua UI è solo verticale: evita foto ruotate in modo imprevisto.\n- **Risoluzione adattiva**: usa `ResolutionPreset.medium` sui dispositivi di fascia bassa per ridurre il consumo di memoria.\n- **Liberare lo spazio**: cancella periodicamente i file temporanei generati da `takePicture()` nella cache dir.\n- Ricorda che `CameraPreview` non va mai inserito in un `ListView` che lo ricostruisce di continuo: mantieni un solo controller attivo per volta.\n- Per test automatici, astrai le chiamate dietro un servizio (es. `CameraService`) così da poterlo mockare: il plugin non funziona negli unit test.","class PicturePreviewPage extends StatelessWidget {\n  const PicturePreviewPage({super.key, required this.file});\n\n  final File file;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      backgroundColor: Colors.black,\n      body: SafeArea(\n        child: Column(\n          children: [\n            Expanded(child: Center(child: Image.file(file))),\n            Padding(\n              padding: const EdgeInsets.all(16),\n              child: Row(\n                mainAxisAlignment: MainAxisAlignment.spaceEvenly,\n                children: [\n                  TextButton.icon(\n                    onPressed: () => Navigator.pop(context, false),\n                    icon: const Icon(Icons.refresh, color: Colors.white),\n                    label: const Text('Ripeti',\n                        style: TextStyle(color: Colors.white)),\n                  ),\n                  FilledButton.icon(\n                    onPressed: () => Navigator.pop(context, true),\n                    icon: const Icon(Icons.check),\n                    label: const Text('Usa questa foto'),\n                  ),\n                ],\n              ),\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n\n\u002F\u002F Nel metodo di scatto, dopo aver salvato il file:\nFuture\u003Cvoid> _confirmShot(File saved) async {\n  final accepted = await Navigator.push\u003Cbool>(\n    context,\n    MaterialPageRoute(builder: (_) => PicturePreviewPage(file: saved)),\n  );\n  if (accepted != true) {\n    await saved.delete();\n    if (mounted) setState(() => _lastPicture = null);\n  }\n}","Dopo lo scatto si apre l'anteprima a schermo intero: confermando il file resta salvato, scegliendo \"Ripeti\" viene cancellato e si torna alla fotocamera pronta per un nuovo scatto.",[],1792664877253]