By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.
Use Drift. It sits on top of sqflite (or native SQLite) and adds type safety, reactive watch() queries, generated row classes, and managed migrations — for free. Raw sqflite means hand-writing SQL strings, manually mapping rows to objects, and no reactivity. There is almost no scenario where a production Flutter app should use raw sqflite directly.
The only reason to touch sqflite directly is if you are evaluating a raw query in a reproducible script. For an app, Drift is the abstraction layer sqflite always needed.
| Feature | Drift | sqflite |
|---|---|---|
| Layer | ORM over sqflite/sqlite3 | Raw SQLite binding |
| Query style | Type-safe DSL + raw SQL fallback | Raw SQL strings |
| Compile-time checks | Yes (generated) | No (runtime errors) |
| Row mapping | Generated classes | Manual Map<String,dynamic> |
| Reactive queries | watch() Stream | None (poll/manual) |
| Migrations | Versioned, typed strategy | Hand-written onUpgrade |
| Boilerplate | Low (generated) | High (manual mapping) |
| Raw SQL escape hatch | Yes (customSelect) | Native |
| Best for | Any production app | Scripts, legacy, tiny apps |
The sqflite package is a thin binding over the native SQLite plugin. You open a database, execute raw SQL strings, and read back List<Map<String,dynamic>>:
final db = await openDatabase('app.db');
await db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
final rows = await db.rawQuery('SELECT * FROM users WHERE name = ?', ['Aisha']);
Functional, but every query is a stringly-typed contract. Rename a column and the compiler will not warn you — you discover it at runtime.
Drift is a layer over sqflite (or the newer sqlite3 native backend). You define tables as Dart classes, and code generation produces typed row classes, companions, and a database class:
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
final user = await (select(users)..where((u) => u.name.equals('Aisha'))).getSingle();
Rename name and the build fails. That is the difference.
This is the decisive feature. select(users).watch() returns a Stream that re-emits whenever the table changes. sqflite has no equivalent — you poll, or build your own invalidation layer. For reactive Flutter UI that updates when local data changes, Drift removes an entire class of plumbing.
Drift migrations are versioned and explicit:
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: (m, from, to) async {
if (from < 2) await m.addColumn(users, users.email);
},
);
sqflite forces you to hand-write onUpgrade with raw ALTER TABLE strings and version constants. Doable, error-prone, and where most production data loss happens.
Rarely. If you are building a throwaway script, a tiny app with one table and no queries, or you have an existing SQL-heavy codebase you cannot refactor — maybe. Even then, wrapping new tables in Drift incrementally is usually worth it.
Drift makes your data layer testable in ways raw sqflite cannot match. You can run the entire database in memory (NativeDatabase.memory()) for tests — no file cleanup, no state leakage between test cases — and inject it through a repository interface. Every query becomes a pure, deterministic function of the data you seed. With raw sqflite, you are managing temp database files and teardown logic in every test file. For a team that unit-tests repositories and use cases against a real SQL engine, this in-memory mode is a quiet but significant advantage.
final db = await openDatabase('app.db');
Future<List<Map<String,dynamic>>> getUsers() {
return db.rawQuery('SELECT * FROM users');
}
// no reactivity — UI must manually refetch after writes
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
// reactive — UI rebuilds automatically on change
Stream<List<User>> watchUsers() => select(users).watch();
The sqflite version gives you a Future of raw maps. The Drift version gives you a typed Stream that updates the UI for free. That is the entire argument.
Choose Drift when: you are building a real app with a database. The type safety, reactive queries, and managed migrations save hours of bugs. This is the default; reach for it unless you have a specific reason not to.
Choose sqflite when: you are writing a one-off script, maintaining a legacy codebase that already uses it, or have a single trivial table where code generation feels disproportionate. Even then, consider Drift for any new table.
Negligibly. Drift adds a thin mapping layer over the same SQLite engine. For hot-path queries you can always drop to raw SQL via customSelect. The productivity and safety gains vastly outweigh the microseconds of overhead.
Yes — it uses build_runner to generate row classes and the database. This is a one-time dart run build_runner build in your workflow (we run it in CI). The generated code is what gives you compile-time safety.
Yes. Drift exposes customSelect/customStatement for arbitrary SQL, and you can map results to generated classes. You get the escape hatch without losing the typed API for the 95% case.
Yes, but it is a real migration, not a drop-in. You define your existing tables as Drift table classes, set the schema version to match your current database, and write a no-op migration strategy (onUpgrade that does nothing if the version matches) so Drift opens the existing .db file without recreating it. Then you replace raw queries one repository at a time. The risk is schema drift between your Drift table definitions and what is actually on disk — version your migration carefully and test against a real production database copy before shipping.
Yes — this is our exact production stack. PowerSync syncs into a local SQLite database, and Drift wraps that same database with typed queries and reactive watch() streams. You define Drift tables that mirror the PowerSync sync schema, and the UI reads through Drift while PowerSync writes flow in underneath. The one caveat: PowerSync writes bypass Drift's streaming updates, so use Drift's watch() on the underlying tables to get reactive UI updates when sync data lands.
Available for hire. Abdelrahman Saed is a Senior Mobile Engineer (Flutter) — open to full-time, fractional, contract, or advisory work. Hire me →
Book a 20-minute call · Download the CV (PDF) · See how I work