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()sulCameraControlleredispose()sull'AudioRecorderquando non servono più. - Gestisci il ciclo di vita dell'app: usa
WidgetsBindingObserverper rilasciare la fotocamera quando l'app va in background (AppLifecycleState.inactive) e reinizializzarla al ritorno. - Verifica lo stato prima di agire: controlla
_controller.value.isInitializedeisRecordingVideoper evitare eccezioni. - Salva i file in modo persistente: gli
XFilerestituiti sono temporanei; copiali ingetApplicationDocumentsDirectory()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.
