Perché gestire i file nel file system
Molte app hanno bisogno di scrivere dati direttamente su disco: esportare report PDF, salvare immagini generate, mettere in cache download temporanei o memorizzare configurazioni complesse. Mentre shared_preferences va bene per piccole chiavi-valore e i database come Isar o Drift per dati strutturati, spesso serve accedere direttamente al file system.
Il problema è che ogni piattaforma organizza le cartelle in modo diverso: su Android non puoi scrivere ovunque, su iOS le app sono sandboxate e su desktop le convenzioni cambiano ancora. Il pacchetto ufficiale path_provider risolve questo problema esponendo le directory corrette in modo cross-platform.
Installazione
Aggiungi le dipendenze al pubspec.yaml:
dependencies:
path_provider: ^2.1.4
path: ^1.9.0
Il pacchetto path non è obbligatorio ma è utilissimo per costruire percorsi in modo sicuro con join, evitando errori con i separatori.
Le directory principali
path_provider espone diverse cartelle, ognuna con uno scopo preciso:
getApplicationDocumentsDirectory(): file persistenti dell'utente, non cancellati dal sistema. Ideale per dati che devono sopravvivere.getTemporaryDirectory(): cache temporanea, il sistema può svuotarla in qualsiasi momento.getApplicationSupportDirectory(): file di supporto non visibili all'utente.getApplicationCacheDirectory(): cache dedicata (disponibile dalla 2.1.0).getExternalStorageDirectory(): solo Android, memoria esterna dell'app.
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'dart:io';
Future<File> _localFile(String name) async {
final dir = await getApplicationDocumentsDirectory();
return File(p.join(dir.path, name));
}
Scrivere e leggere file di testo
Una volta ottenuto l'oggetto File, puoi usare le API standard di dart:io:
Future<void> saveNote(String content) async {
final file = await _localFile('note.txt');
await file.writeAsString(content);
}
Future<String> readNote() async {
try {
final file = await _localFile('note.txt');
return await file.readAsString();
} on FileSystemException {
return '';
}
}
Gestire l'eccezione FileSystemException è importante: alla prima esecuzione il file potrebbe non esistere ancora.
Lavorare con dati binari
Per immagini o PDF si usano i byte grezzi tramite Uint8List:
import 'dart:typed_data';
Future<File> saveBytes(String name, Uint8List bytes) async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, name));
return file.writeAsBytes(bytes);
}
Future<Uint8List?> readBytes(String name) async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, name));
if (await file.exists()) {
return file.readAsBytes();
}
return null;
}
Gestire la cache in modo pulito
La cartella temporanea è perfetta per download che possono essere rigenerati. Ecco un esempio che scarica e mette in cache un file, riutilizzandolo se già presente:
Future<File> cachedDownload(String url, String fileName) async {
final tempDir = await getTemporaryDirectory();
final file = File(p.join(tempDir.path, fileName));
if (await file.exists()) {
return file; // già in cache
}
final response = await HttpClient().getUrl(Uri.parse(url))
.then((req) => req.close());
final bytes = await consolidateHttpClientResponseBytes(response);
await file.writeAsBytes(bytes);
return file;
}
Svuotare la cache
È buona pratica offrire all'utente un modo per liberare spazio:
Future<void> clearCache() async {
final tempDir = await getTemporaryDirectory();
if (await tempDir.exists()) {
await tempDir.delete(recursive: true);
}
}
Elencare i file di una cartella
Per mostrare all'utente i file salvati puoi iterare sul contenuto di una directory:
Future<List<File>> listSavedFiles() async {
final dir = await getApplicationDocumentsDirectory();
final entities = dir.listSync();
return entities.whereType<File>().toList();
}
Usa listSync() con cautela su cartelle molto grandi: in quei casi preferisci lo stream asincrono dir.list().
Best practice
- Non salvare percorsi assoluti in un database: su iOS il path della sandbox può cambiare tra un aggiornamento e l'altro. Memorizza solo il nome del file e ricostruisci il percorso a runtime.
- Scegli la directory giusta: usa i documenti per dati importanti e la cache per contenuti rigenerabili, così eviti di occupare spazio non liberabile dal sistema.
- Gestisci sempre le eccezioni di I/O: dischi pieni e permessi mancanti sono scenari reali.
- Sposta operazioni pesanti in un isolate con
computese scrivi o leggi file di grandi dimensioni, per non bloccare la UI. - Su Flutter Web
path_providernon funziona: usa soluzioni basate suIndexedDBo su download del browser.
Conclusione
La gestione del file system in Flutter è più semplice di quanto sembri grazie a path_provider, che astrae le differenze tra le piattaforme. Combinando le directory corrette con le API di dart:io e il pacchetto path puoi costruire funzionalità robuste di persistenza, export e caching, mantenendo il codice pulito e portabile.