Cos'è Isar

Quando si parla di persistenza locale in Flutter, spesso si pensa subito a SQLite o a soluzioni relazionali. Ma esiste un'alternativa NoSQL nata specificamente per Flutter e Dart: Isar. È un database embedded estremamente veloce, con supporto a query tipizzate, indici composti, transazioni e — cosa fondamentale — la reattività tramite watch.

A differenza di soluzioni come Hive, Isar è pensato anche per dataset di grandi dimensioni, offre query complesse e un ottimo supporto agli indici, mantenendo comunque un'API pulita e generata automaticamente.

Perché sceglierlo

  • Performance: scritto in Rust, con benchmark che lo pongono tra i più rapidi.
  • Type-safe: le query sono generate in base ai modelli, con autocompletamento.
  • Reattivo: puoi ascoltare i cambiamenti su collezioni e singoli oggetti.
  • Cross-platform: iOS, Android, desktop e web.
  • Zero configurazione SQL: nessuna gestione manuale di schemi o migrazioni complesse.

Installazione

Aggiungi le dipendenze al pubspec.yaml:

dependencies:
  isar: ^3.1.0
  isar_flutter_libs: ^3.1.0
  path_provider: ^2.1.0

dev_dependencies:
  isar_generator: ^3.1.0
  build_runner: ^2.4.0

Definire un modello

Le collezioni si dichiarano con l'annotazione @collection. Ogni oggetto ha bisogno di un campo id di tipo Id.

import 'package:isar/isar.dart';

part 'ricetta.g.dart';

@collection
class Ricetta {
  Id id = Isar.autoIncrement;

  @Index(caseSensitive: false)
  late String titolo;

  late int minutiPreparazione;

  @Index()
  late List<String> tag;

  bool preferita = false;
}

Dopo aver definito i modelli, genera il codice:

dart run build_runner build

Questo crea il file ricetta.g.dart con schema e query tipizzate.

Aprire il database

import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';

Future<Isar> apriDatabase() async {
  final dir = await getApplicationDocumentsDirectory();
  return Isar.open(
    [RicettaSchema],
    directory: dir.path,
  );
}

Conviene mantenere una singola istanza di Isar per tutta la durata dell'app, ad esempio tramite un service o l'iniezione delle dipendenze.

Scrivere dati

Tutte le operazioni di scrittura avvengono all'interno di una transazione:

final ricetta = Ricetta()
  ..titolo = 'Pasta al pomodoro'
  ..minutiPreparazione = 20
  ..tag = ['primo', 'veloce'];

await isar.writeTxn(() async {
  await isar.ricettas.put(ricetta); // insert o update
});

Il metodo put gestisce sia l'inserimento sia l'aggiornamento in base all'id.

Leggere e interrogare

Le query sono fluide e tipizzate:

// Tutte le ricette
final tutte = await isar.ricettas.where().findAll();

// Filtri con condizioni
final veloci = await isar.ricettas
    .filter()
    .minutiPreparazioneLessThan(30)
    .and()
    .preferitaEqualTo(true)
    .findAll();

// Ricerca per tag e ordinamento
final primi = await isar.ricettas
    .filter()
    .tagElementEqualTo('primo')
    .sortByTitolo()
    .findAll();

Reattività con watch

Uno dei punti di forza di Isar è la possibilità di reagire ai cambiamenti. Perfetto da combinare con StreamBuilder o con soluzioni di state management.

Stream<List<Ricetta>> osservaPreferite() {
  return isar.ricettas
      .filter()
      .preferitaEqualTo(true)
      .watch(fireImmediately: true);
}

Nell'interfaccia:

StreamBuilder<List<Ricetta>>(
  stream: repository.osservaPreferite(),
  builder: (context, snapshot) {
    final ricette = snapshot.data ?? [];
    return ListView.builder(
      itemCount: ricette.length,
      itemBuilder: (_, i) => ListTile(
        title: Text(ricette[i].titolo),
      ),
    );
  },
)

Con fireImmediately: true lo stream emette subito lo stato attuale, poi ogni volta che i dati cambiano.

Eliminare dati

await isar.writeTxn(() async {
  await isar.ricettas.delete(id);
  // oppure con query
  await isar.ricettas.filter().preferitaEqualTo(false).deleteAll();
});

Buone pratiche

  • Isola l'accesso ai dati in un repository, evitando di chiamare Isar direttamente dai widget.
  • Usa gli indici sui campi su cui filtri o ordini spesso: migliorano drasticamente le performance.
  • Riutilizza l'istanza di Isar invece di aprirla più volte.
  • Sfrutta i link (IsarLink e IsarLinks) per modellare relazioni tra collezioni senza chiavi esterne manuali.
  • Per progetti nuovi valuta anche lo stato del maintenance: verifica sempre la versione più recente sul repository ufficiale.

Conclusione

Isar è una scelta eccellente quando serve una persistenza locale veloce, reattiva e priva della complessità di SQL. La combinazione tra query tipizzate, indici e stream lo rende particolarmente adatto ad app che devono aggiornare l'interfaccia in tempo reale al variare dei dati. Integrandolo dietro un repository ottieni un layer dati pulito, testabile e facilmente sostituibile.