Perché Drift per la persistenza locale
Quando un'applicazione Flutter ha bisogno di salvare dati strutturati in locale — pensiamo a una to-do list offline, a una cache complessa o a un'app di tracking — SharedPreferences non basta. In questi casi serve un vero database SQLite, e Drift (l'evoluzione di Moor) è oggi una delle scelte più solide.
Drift offre tre vantaggi chiave:
- Type-safety: le query vengono validate a tempo di compilazione, niente più errori SQL scoperti a runtime.
- Reattività: puoi osservare le query come
Stream, aggiornando automaticamente la UI quando i dati cambiano. - Cross-platform: funziona su mobile, desktop e web.
Installazione
Aggiungi le dipendenze al pubspec.yaml:
dependencies:
drift: ^2.18.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.1.0
path: ^1.9.0
dev_dependencies:
drift_dev: ^2.18.0
build_runner: ^2.4.0
Definire una tabella
In Drift le tabelle sono classi Dart. Definiamo una tabella Todos:
import 'package:drift/drift.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 100)();
TextColumn get description => text().nullable()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
Creare il database
La classe del database collega le tabelle e gestisce la connessione:
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
part 'database.g.dart';
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, 'app.db'));
return NativeDatabase.createInBackground(file);
});
}
Genera il codice con:
dart run build_runner build
Operazioni CRUD
Drift genera automaticamente le classi dei dati. Ecco i metodi principali da aggiungere alla classe AppDatabase:
// Lettura reattiva: la UI si aggiorna da sola
Stream<List<Todo>> watchAllTodos() => select(todos).watch();
// Inserimento
Future<int> addTodo(TodosCompanion entry) => into(todos).insert(entry);
// Aggiornamento
Future<bool> updateTodo(Todo todo) => update(todos).replace(todo);
// Eliminazione
Future<int> deleteTodo(int id) =>
(delete(todos)..where((t) => t.id.equals(id))).go();
Per inserire un nuovo elemento si usa il Companion, che distingue i campi presenti da quelli assenti:
await db.addTodo(
TodosCompanion.insert(
title: 'Studiare Drift',
description: const Value('Leggere la documentazione'),
),
);
Mostrare i dati con StreamBuilder
Grazie alla reattività, basta uno StreamBuilder per avere una lista sempre aggiornata:
StreamBuilder<List<Todo>>(
stream: db.watchAllTodos(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, i) {
final todo = todos[i];
return CheckboxListTile(
title: Text(todo.title),
value: todo.completed,
onChanged: (val) => db.updateTodo(
todo.copyWith(completed: val ?? false),
),
);
},
);
},
)
Gestire le migrazioni
Quando lo schema cambia, incrementa schemaVersion e definisci la strategia di migrazione:
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
if (from < 2) {
await migrator.addColumn(todos, todos.priority);
}
},
);
Conclusione
Drift unisce la potenza di SQLite alla sicurezza dei tipi di Dart e a un modello reattivo che si integra naturalmente con l'architettura di Flutter. È la soluzione ideale quando hai bisogno di query complesse, relazioni e persistenza affidabile, mantenendo al tempo stesso un codice pulito e verificato a compile-time. Se la tua app cresce oltre le semplici preferenze, Drift è un investimento che ripaga.
