Drift: Type-Safe, Reactive SQL for Flutter
Drift wraps SQLite in generated, type-safe Dart with reactive query streams. How it works and when it beats sqflite or key-value stores.
Published on • October 18, 2026
AI Assistant

If shared_preferences is a hammer and raw sqflite is a hand-forged chisel, Drift is a full woodworking shop: a reactive persistence library built on SQLite where your tables become Dart classes, your queries get checked at compile time, and any query can turn into a stream that updates your UI. It’s a Flutter Favorite with broad platform support — Android, iOS, macOS, Windows, Linux, and the web — and it’s one of the few Flutter persistence options with built-in isolate support for threading. Here’s how it fits together.
The core idea: tables as code, SQL checked at compile time
You define your schema as abstract Dart classes extending Table, and drift’s code generator produces row classes, type-safe query builders, and a database class:
import 'package:drift/drift.dart';
part 'app_database.g.dart';
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 1, max: 120)();
BoolColumn get done => boolean().withDefault(const Constant(false))();
DateTimeColumn get dueAt => dateTime().nullable()();
}
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
Stream<List<Todo>> watchPending() =>
(select(todos)..where((t) => t.done.equals(false))).watch();
}
LazyDatabase _openConnection() => LazyDatabase(() async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, 'app.db'));
return NativeDatabase.createInBackground(file);
});
Run dart run build_runner build and you get a Todo data class with real field types, an insert/update/delete API, and a watchPending() stream. Note the createInBackground call: drift runs its database work on a background isolate by default, so queries never jank your UI thread — the package description bills it as the only major Flutter persistence library with built-in threading support.
For most Flutter apps, the drift_flutter package’s driftDatabase(name: 'app') helper sets up paths, isolates, and web support in one line — use that unless you need custom wiring.
Reactive queries: SQL that updates your UI
The standout feature is watch(). Any query — filters, joins, aggregations — becomes a Stream that re-emits whenever the underlying tables change. Feed it straight to a StreamBuilder:
StreamBuilder<List<Todo>>(
stream: db.watchPending(),
builder: (context, snapshot) {
final todos = snapshot.data ?? const [];
return ListView.builder(
itemCount: todos.length,
itemBuilder: (_, i) => CheckboxListTile(
value: todos[i].done,
title: Text(todos[i].title),
onChanged: (v) => (db.update(db.todos)
..where((t) => t.id.equals(todos[i].id)))
.write(TodosCompanion(done: Value(v ?? false))),
),
);
},
);
No manual cache invalidation, no “refresh after write” plumbing — insert a row anywhere in the app and every stream over that table re-fires.
SQL when you want it, Dart when you don’t
Drift deliberately doesn’t force one style. The fluent builder above is type-safe Dart; when a query gets gnarly, write SQL and still get type safety:
final results = await db.customSelect(
'SELECT title, COUNT(*) AS n FROM todos WHERE done = 1 GROUP BY substr(title, 1, 3)',
readsFrom: {db.todos},
).map((row) => (row.read<String>('title'), row.read<int>('n'))).get();
The readsFrom set is what keeps the reactive magic working for custom SQL. Drift also supports transactions, batched writes, joins, and advanced SQL features like WITH and WINDOW clauses, with generated lints catching mistakes at compile time rather than at runtime on a user’s device.
Migrations without fear
Bumping schemaVersion triggers the MigrationStrategy hook, where upgrades are explicit:
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) {
await m.addColumn(todos, todos.dueAt);
}
},
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
},
);
Drift ships schema-dump tooling (drift_dev schema steps) that generates migration steps and lets you verify them in tests — you can assert that an old database file upgrades to the new schema without losing rows.
DAOs and structure
For anything beyond a toy app, drift’s data access objects keep the database code modular. A DAO is a class annotated with @DriftAccessor that owns a slice of your schema:
@DriftAccessor(tables: [Todos])
class TodoDao extends DatabaseAccessor<AppDatabase> with _$TodoDaoMixin {
TodoDao(super.db);
Future<int> complete(int id) =>
(update(todos)..where((t) => t.id.equals(id)))
.write(const TodosCompanion(done: Value(true)));
}
Group queries by feature, inject DAOs into your repositories, and the database class stays a thin wiring layer.
Web and platforms
Drift runs everywhere Flutter does. On the web it uses SQLite compiled to WebAssembly (paired with package:web), and there’s a worker setup so database work doesn’t block the main thread — relevant if you’re also considering the WebAssembly push on Flutter Web. The repo includes a cross-platform todo app example demonstrating one database across all six stable platforms.
When to pick drift (and when not to)
Pick drift when you have relational data: entities with keys, joins, filters, aggregates, or multi-table invariants — offline-first caches, note apps, shopping carts, anything that outgrows a key-value store. Its type safety and migration tooling pay off precisely when your schema will evolve.
Skip it when your data really is a handful of settings — use shared_preferences or flutter_secure_storage — or when you need a backend-sync engine rather than a local database (pair drift with a sync layer instead; PowerSync and Stream both sponsor drift precisely because they interoperate with it).
Getting started is three steps: add drift and drift_flutter plus build_runner/drift_dev as dev dependencies, define your tables, run the code generator. The drift documentation goes deep from there, and the pub.dev page has the full feature rundown.