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 compute se scrivi o leggi file di grandi dimensioni, per non bloccare la UI.
  • Su Flutter Web path_provider non funziona: usa soluzioni basate su IndexedDB o 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.