Introduzione

Molte app moderne richiedono l'accesso a fotocamera e microfono: dai social network alle app di note vocali, passando per lettori di QR code e videochiamate. In questa guida vedremo come integrare la cattura di foto, video e audio in Flutter usando due package solidi e mantenuti: camera e record.

Installazione

Aggiungiamo le dipendenze al pubspec.yaml:

dependencies:
  camera: ^0.11.0
  record: ^5.1.2
  path_provider: ^2.1.4
  permission_handler: ^11.3.1

Configurazione dei permessi

Su Android (android/app/src/main/AndroidManifest.xml):

<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

Assicurati inoltre che minSdkVersion sia almeno 21.

Su iOS (ios/Runner/Info.plist):

<key>NSCameraUsageDescription</key>
<string>L'app usa la fotocamera per scattare foto e registrare video.</string>
<key>NSMicrophoneUsageDescription</key>
<string>L'app usa il microfono per registrare audio.</string>

Anteprima della fotocamera

Il package camera fornisce un CameraController che gestisce l'inizializzazione del dispositivo e la sua anteprima. Il primo passo è recuperare la lista delle fotocamere disponibili.

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';

late List<CameraDescription> cameras;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  cameras = await availableCameras();
  runApp(const MyApp());
}

Ora creiamo un widget con stato che inizializza il controller:

class CameraScreen extends StatefulWidget {
  const CameraScreen({super.key});

  @override
  State<CameraScreen> createState() => _CameraScreenState();
}

class _CameraScreenState extends State<CameraScreen> {
  late CameraController _controller;
  late Future<void> _initFuture;

  @override
  void initState() {
    super.initState();
    _controller = CameraController(
      cameras.first,
      ResolutionPreset.high,
      enableAudio: true,
    );
    _initFuture = _controller.initialize();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _initFuture,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          return CameraPreview(_controller);
        }
        return const Center(child: CircularProgressIndicator());
      },
    );
  }
}

Attenzione: ricordati sempre di chiamare dispose() sul controller per liberare le risorse hardware, altrimenti la fotocamera resterà bloccata anche uscendo dalla schermata.

Scattare una foto

Una volta inizializzato il controller, catturare un'immagine richiede una sola chiamata:

Future<void> _scattaFoto() async {
  try {
    await _initFuture;
    final XFile foto = await _controller.takePicture();
    debugPrint('Foto salvata in: ${foto.path}');
    // foto.path punta a un file temporaneo
  } catch (e) {
    debugPrint('Errore durante lo scatto: $e');
  }
}

Il metodo restituisce un XFile, un'astrazione cross-platform che espone il percorso del file e i suoi byte tramite readAsBytes().

Registrare un video

La registrazione video usa startVideoRecording e stopVideoRecording:

bool _inRegistrazione = false;

Future<void> _avviaVideo() async {
  await _initFuture;
  await _controller.startVideoRecording();
  setState(() => _inRegistrazione = true);
}

Future<void> _fermaVideo() async {
  final XFile video = await _controller.stopVideoRecording();
  setState(() => _inRegistrazione = false);
  debugPrint('Video salvato in: ${video.path}');
}

Registrare solo audio con record

Quando serve solo l'audio (es. note vocali) il package camera è eccessivo. Il package record è leggero e specializzato.

import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

final _recorder = AudioRecorder();

Future<void> _iniziaAudio() async {
  if (await _recorder.hasPermission()) {
    final dir = await getApplicationDocumentsDirectory();
    final path = p.join(dir.path, 'nota_${DateTime.now().millisecondsSinceEpoch}.m4a');

    await _recorder.start(
      const RecordConfig(encoder: AudioEncoder.aacLc),
      path: path,
    );
  }
}

Future<String?> _fermaAudio() async {
  final path = await _recorder.stop();
  debugPrint('Audio salvato in: $path');
  return path;
}

AudioRecorder gestisce internamente la richiesta del permesso microfono tramite hasPermission(), semplificando il flusso.

Monitorare l'ampiezza (visualizzazione onde)

Un tocco professionale è mostrare la forma d'onda durante la registrazione. Il package espone uno stream dell'ampiezza:

_recorder
    .onAmplitudeChanged(const Duration(milliseconds: 200))
    .listen((amp) {
  debugPrint('Ampiezza corrente: ${amp.current} dB');
});

Gestione robusta dei permessi

Sebbene entrambi i package gestiscano parzialmente i permessi, è buona pratica controllarli esplicitamente prima di aprire l'interfaccia, per offrire un messaggio chiaro all'utente:

import 'package:permission_handler/permission_handler.dart';

Future<bool> _verificaPermessi() async {
  final statuses = await [
    Permission.camera,
    Permission.microphone,
  ].request();

  final tuttiConcessi = statuses.values.every((s) => s.isGranted);

  if (statuses.values.any((s) => s.isPermanentlyDenied)) {
    await openAppSettings();
  }
  return tuttiConcessi;
}

Best practice

  • Rilascia sempre le risorse: chiama dispose() sul CameraController e dispose() sull'AudioRecorder quando non servono più.
  • Gestisci il ciclo di vita dell'app: usa WidgetsBindingObserver per rilasciare la fotocamera quando l'app va in background (AppLifecycleState.inactive) e reinizializzarla al ritorno.
  • Verifica lo stato prima di agire: controlla _controller.value.isInitialized e isRecordingVideo per evitare eccezioni.
  • Salva i file in modo persistente: gli XFile restituiti sono temporanei; copiali in getApplicationDocumentsDirectory() se devono sopravvivere alla sessione.

Esempio di gestione del lifecycle

class _CameraScreenState extends State<CameraScreen>
    with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (!_controller.value.isInitialized) return;
    if (state == AppLifecycleState.inactive) {
      _controller.dispose();
    } else if (state == AppLifecycleState.resumed) {
      _initFuture = _controller.initialize();
    }
  }
}

Conclusioni

Con camera e record puoi coprire la stragrande maggioranza dei casi d'uso multimediali in Flutter, mantenendo il codice cross-platform. Ricorda che la vera sfida non è tanto scattare una foto, quanto gestire correttamente permessi, ciclo di vita e risorse hardware: seguendo le best practice viste qui eviterai i classici crash da fotocamera bloccata e offrirai un'esperienza fluida su Android e iOS.