← Flutter Reference

Drift vs sqflite

Data & Storage

By Abdelrahman Saed — Senior Mobile Engineer. Last updated: 2026-08-10.

Quick answer

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 comparison

FeatureDriftsqflite
LayerORM over sqflite/sqlite3Raw SQLite binding
Query styleType-safe DSL + raw SQL fallbackRaw SQL strings
Compile-time checksYes (generated)No (runtime errors)
Row mappingGenerated classesManual Map<String,dynamic>
Reactive querieswatch() StreamNone (poll/manual)
MigrationsVersioned, typed strategyHand-written onUpgrade
BoilerplateLow (generated)High (manual mapping)
Raw SQL escape hatchYes (customSelect)Native
Best forAny production appScripts, legacy, tiny apps

Detailed comparison

What sqflite gives you

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.

What Drift adds

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.

Reactive queries

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.

Migrations

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.

When raw sqflite still makes sense

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.

Testing and DI

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.

Code comparison

Reactive user query — sqflite

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

Reactive user query — Drift

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.

Which should you choose?

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.

Related definitions

Related reading

FAQ

Is Drift slower than sqflite?

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.

Does Drift require code generation?

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.

Can I use raw SQL in Drift when I need to?

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.

Can I migrate an existing sqflite app to Drift incrementally?

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.

Does Drift work well with PowerSync?

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