sqflite_ffi: ffi sqflite as a Flutter plugin
package:sqflite_ffi wraps sqflite_common_ffi in a Dart-only Flutter
plugin. Adding it to a Flutter app does two things that sqflite_common_ffi
alone does not:
- At startup Flutter calls
SqfliteFfiPlugin.registerWith(), which runs
sqfliteFfiInit() and sets sqfliteDatabaseFactoryFfi as the global
databaseFactory if none is registered yet. The global openDatabase()
works on Windows, Linux, macOS, Android and iOS with no main() code.
- All Flutter isolates (main,
compute, Isolate.run) share one sqflite
isolate: its SendPort is registered in IsolateNameServer under
sqfliteFfiIsolatePortName, so singleInstance and transaction ordering
hold across isolates.
import 'package:flutter/widgets.dart';
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
var db = await openDatabase(inMemoryDatabasePath);
debugPrint('${await db.rawQuery('SELECT sqlite_version()')}');
await db.close();
runApp(const SizedBox());
}
Guidelines
- Add
sqflite_ffi (0.1.x, Flutter >= 3.44, Dart 3.12) to dependencies.
It brings sqflite_common_ffi and sqlite3 (>= 3, build hooks: SQLite is
bundled, nothing to install; run flutter clean when changing the
sqlite3 major version).
- Import
package:sqflite_ffi/sqflite_ffi.dart. It re-exports
sqflite_common_ffi/sqflite_ffi.dart (hence the whole sqflite_common
API: Database, openDatabase, deleteDatabase, inMemoryDatabasePath,
databaseFactory, sqfliteFfiInit, SqfliteFfiInit,
databaseFactoryFfiNoIsolate, SqfliteFfiIsolatePortServer) but hides
databaseFactoryFfi and createDatabaseFactoryFfi. Use the package's own
sqfliteDatabaseFactoryFfi and createSqfliteDatabaseFactoryFfi instead.
- Choosing between packages:
sqflite alone: native plugin, iOS/Android/macOS only.
sqflite_common_ffi: pure Dart, desktop and tests; you set
databaseFactory yourself; each Dart isolate gets its own sqflite
isolate.
sqflite_ffi: Flutter app that wants ffi everywhere (or on desktop with
zero setup) and/or uses the database from several isolates.
sqflite_common_ffi_web for the web: sqflite_ffi is a no-op there
(SqfliteFfiPlugin.registerWith() does nothing and
sqfliteDatabaseFactoryFfi returns the unsupported ffi factory).
- Registration order:
SqfliteFfiPlugin.registerWith() uses
databaseFactoryOrNull ??=, and so does the native sqflite plugin
(SqflitePlugin.registerWith()). When both plugins are in the app the
first one in the generated plugin registrant wins, which is not something
to rely on. Assign the factory explicitly in main(): databaseFactory = sqfliteDatabaseFactoryFfi; to force ffi, or databaseFactory = databaseFactorySqflitePlugin; (exported by package:sqflite/sqflite.dart)
to force the native plugin on mobile.
- Explicit factory use is always possible:
sqfliteDatabaseFactoryFfi.openDatabase(path, options: ...). Prefer it
in libraries and inject it for tests.
createSqfliteDatabaseFactoryFfi({SqfliteFfiInit? ffiInit}) returns a new
factory that still shares the isolate through IsolateNameServer.
ffiInit must be top-level or static and runs in the sqflite isolate
before the first SQLite call; the native library itself is selected by the
sqlite3 build hook user defines in pubspec.yaml
(hooks.user_defines.sqlite3.source), see sqflite-common-ffi-desktop.
- Do not assume plugin registration in background isolates (
compute,
Isolate.run, Isolate.spawn): the global databaseFactory may be unset
there. Either call DartPluginRegistrant.ensureInitialized() (from
dart:ui) before the global openDatabase(), or use
sqfliteDatabaseFactoryFfi directly: the IsolateNameServer lookup works
in any isolate without registration.
- When two isolates open the same file, pass
OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false) (the default
is true in debug mode) so the second openDatabase does not roll back a
transaction running in the first isolate. Do not close() a shared
single-instance database from the background isolate; the owner closes it.
- Stale registrations (hot restart leaves a dead port in
IsolateNameServer) are detected with a ping (2 s timeout) and replaced
automatically; no code needed.
- Paths: relative paths resolve under
.dart_tool/sqflite_common_ffi/databases
in the current directory. In an app use path_provider
(getApplicationSupportDirectory()) and package:path join. The parent
directory is created on open.
- Tests:
flutter test works with sqfliteDatabaseFactoryFfi after
TestWidgetsFlutterBinding.ensureInitialized() and sqfliteFfiInit();
plain sqflite_common_ffi in dev_dependencies is enough when the test
does not need isolate sharing (see sqflite-common-ffi-testing).
Examples
Database work in a compute isolate sharing the instance
import 'package:flutter/foundation.dart' show compute;
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<void> _insertInIsolate(String path) async {
// Same sqflite isolate as the main isolate: same Database instance.
final db = await sqfliteDatabaseFactoryFfi.openDatabase(
path,
options: OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false),
);
await db.transaction((txn) async {
await txn.insert('Test', {'name': 'isolate 1'});
await txn.insert('Test', {'name': 'isolate 2'});
});
// Do not close: the main isolate owns the shared instance.
}
Future<List<Object?>> run(String path) async {
final db = await sqfliteDatabaseFactoryFfi.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, _) => db.execute(
'CREATE TABLE Test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',
),
rollbackActiveTransactionOnOpen: false,
),
);
try {
await Future.wait([
db.transaction((txn) async {
await txn.insert('Test', {'name': 'main 1'});
await Future<void>.delayed(const Duration(milliseconds: 100));
await txn.insert('Test', {'name': 'main 2'});
}),
compute(_insertInIsolate, path),
]);
// The background transaction waited for the main one:
// main 1, main 2, isolate 1, isolate 2
return (await db.query('Test', orderBy: 'id')).map((r) => r['name']).toList();
} finally {
await db.close();
}
}
Forcing ffi even when the native sqflite plugin is present
import 'package:flutter/widgets.dart';
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Both sqflite and sqflite_ffi are in pubspec.yaml; pick ffi explicitly.
databaseFactory = sqfliteDatabaseFactoryFfi;
runApp(const SizedBox());
}
Database path with path_provider
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<Database> openAppDatabase() async {
final dir = await getApplicationSupportDirectory();
return openDatabase(
p.join(dir.path, 'app.db'),
version: 1,
onCreate: (db, _) => db.execute(
'CREATE TABLE Note (id INTEGER PRIMARY KEY, text TEXT)',
),
);
}
Isolate spawned outside Flutter
import 'dart:isolate';
import 'dart:ui' show DartPluginRegistrant;
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<int> countInIsolate(String path) => Isolate.run(() async {
// Needed for the global openDatabase(); not for sqfliteDatabaseFactoryFfi.
DartPluginRegistrant.ensureInitialized();
final db = await openDatabase(
path,
options: OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false),
);
final rows = await db.rawQuery('SELECT COUNT(*) AS c FROM Note');
return rows.first['c'] as int;
});
Custom ffiInit
import 'package:sqflite_ffi/sqflite_ffi.dart';
void _ffiInit() {
// Runs inside the shared sqflite isolate before the first SQLite call.
}
final myFactory = createSqfliteDatabaseFactoryFfi(ffiInit: _ffiInit);
Flutter test
import 'package:flutter_test/flutter_test.dart';
import 'package:sqflite_ffi/sqflite_ffi.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
sqfliteFfiInit();
test('open in memory', () async {
final db = await sqfliteDatabaseFactoryFfi.openDatabase(inMemoryDatabasePath);
expect(await db.getVersion(), 0);
await db.close();
});
}
Common mistakes
- Depending on
sqflite_ffi and still calling sqfliteFfiInit() +
databaseFactory = databaseFactoryFfi from sqflite_common_ffi: this
creates a second, non-shared sqflite isolate and prints the "changing
sqflite default factory" warning. Use sqfliteDatabaseFactoryFfi or
nothing.
- Expecting ffi on the web: use
sqflite_common_ffi_web.
- Closing a shared single-instance database from a background isolate.
- Opening the same file from two isolates in debug mode without
rollbackActiveTransactionOnOpen: false.
- Calling
openDatabase() in a hand-spawned isolate without
DartPluginRegistrant.ensureInitialized(): databaseFactory not initialized.
- Having both
sqflite and sqflite_ffi in pubspec.yaml and not
assigning databaseFactory: which implementation runs depends on the
registrant order.
1---2name: sqflite-ffi-flutter3description: Use when a Flutter app should use the ffi (package:sqlite3) implementation of sqflite on desktop and mobile with package:sqflite_ffi: what it adds over sqflite_common_ffi (Dart-only plugin, automatic registration of sqfliteDatabaseFactoryFfi as the default databaseFactory through SqfliteFfiPlugin.registerWith, one sqflite isolate shared between Flutter isolates via IsolateNameServer and sqfliteFfiIsolatePortName), createSqfliteDatabaseFactoryFfi, using the database from compute / Isolate.run, DartPluginRegistrant.ensureInitialized, coexistence with the native sqflite plugin, and when to pick sqflite, sqflite_common_ffi or sqflite_ffi.4---56# sqflite_ffi: ffi sqflite as a Flutter plugin78`package:sqflite_ffi` wraps `sqflite_common_ffi` in a Dart-only Flutter9plugin. Adding it to a Flutter app does two things that `sqflite_common_ffi`10alone does not:1112* At startup Flutter calls `SqfliteFfiPlugin.registerWith()`, which runs13 `sqfliteFfiInit()` and sets `sqfliteDatabaseFactoryFfi` as the global14 `databaseFactory` if none is registered yet. The global `openDatabase()`15 works on Windows, Linux, macOS, Android and iOS with no `main()` code.16* All Flutter isolates (main, `compute`, `Isolate.run`) share one sqflite17 isolate: its `SendPort` is registered in `IsolateNameServer` under18 `sqfliteFfiIsolatePortName`, so `singleInstance` and transaction ordering19 hold across isolates.2021```dart22import 'package:flutter/widgets.dart';23import 'package:sqflite_ffi/sqflite_ffi.dart';2425Future<void> main() async {26 WidgetsFlutterBinding.ensureInitialized();27 var db = await openDatabase(inMemoryDatabasePath);28 debugPrint('${await db.rawQuery('SELECT sqlite_version()')}');29 await db.close();30 runApp(const SizedBox());31}32```3334## Guidelines3536* Add `sqflite_ffi` (0.1.x, Flutter >= 3.44, Dart 3.12) to `dependencies`.37 It brings `sqflite_common_ffi` and `sqlite3` (>= 3, build hooks: SQLite is38 bundled, nothing to install; run `flutter clean` when changing the39 `sqlite3` major version).40* Import `package:sqflite_ffi/sqflite_ffi.dart`. It re-exports41 `sqflite_common_ffi/sqflite_ffi.dart` (hence the whole `sqflite_common`42 API: `Database`, `openDatabase`, `deleteDatabase`, `inMemoryDatabasePath`,43 `databaseFactory`, `sqfliteFfiInit`, `SqfliteFfiInit`,44 `databaseFactoryFfiNoIsolate`, `SqfliteFfiIsolatePortServer`) but hides45 `databaseFactoryFfi` and `createDatabaseFactoryFfi`. Use the package's own46 `sqfliteDatabaseFactoryFfi` and `createSqfliteDatabaseFactoryFfi` instead.47* Choosing between packages:48 * `sqflite` alone: native plugin, iOS/Android/macOS only.49 * `sqflite_common_ffi`: pure Dart, desktop and tests; you set50 `databaseFactory` yourself; each Dart isolate gets its own sqflite51 isolate.52 * `sqflite_ffi`: Flutter app that wants ffi everywhere (or on desktop with53 zero setup) and/or uses the database from several isolates.54 * `sqflite_common_ffi_web` for the web: `sqflite_ffi` is a no-op there55 (`SqfliteFfiPlugin.registerWith()` does nothing and56 `sqfliteDatabaseFactoryFfi` returns the unsupported ffi factory).57* Registration order: `SqfliteFfiPlugin.registerWith()` uses58 `databaseFactoryOrNull ??=`, and so does the native `sqflite` plugin59 (`SqflitePlugin.registerWith()`). When both plugins are in the app the60 first one in the generated plugin registrant wins, which is not something61 to rely on. Assign the factory explicitly in `main()`: `databaseFactory =62 sqfliteDatabaseFactoryFfi;` to force ffi, or `databaseFactory =63 databaseFactorySqflitePlugin;` (exported by `package:sqflite/sqflite.dart`)64 to force the native plugin on mobile.65* Explicit factory use is always possible:66 `sqfliteDatabaseFactoryFfi.openDatabase(path, options: ...)`. Prefer it67 in libraries and inject it for tests.68* `createSqfliteDatabaseFactoryFfi({SqfliteFfiInit? ffiInit})` returns a new69 factory that still shares the isolate through `IsolateNameServer`.70 `ffiInit` must be top-level or static and runs in the sqflite isolate71 before the first SQLite call; the native library itself is selected by the72 `sqlite3` build hook user defines in `pubspec.yaml`73 (`hooks.user_defines.sqlite3.source`), see `sqflite-common-ffi-desktop`.74* Do not assume plugin registration in background isolates (`compute`,75 `Isolate.run`, `Isolate.spawn`): the global `databaseFactory` may be unset76 there. Either call `DartPluginRegistrant.ensureInitialized()` (from77 `dart:ui`) before the global `openDatabase()`, or use78 `sqfliteDatabaseFactoryFfi` directly: the `IsolateNameServer` lookup works79 in any isolate without registration.80* When two isolates open the same file, pass81 `OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false)` (the default82 is `true` in debug mode) so the second `openDatabase` does not roll back a83 transaction running in the first isolate. Do not `close()` a shared84 single-instance database from the background isolate; the owner closes it.85* Stale registrations (hot restart leaves a dead port in86 `IsolateNameServer`) are detected with a ping (2 s timeout) and replaced87 automatically; no code needed.88* Paths: relative paths resolve under `.dart_tool/sqflite_common_ffi/databases`89 in the current directory. In an app use `path_provider`90 (`getApplicationSupportDirectory()`) and `package:path` `join`. The parent91 directory is created on open.92* Tests: `flutter test` works with `sqfliteDatabaseFactoryFfi` after93 `TestWidgetsFlutterBinding.ensureInitialized()` and `sqfliteFfiInit()`;94 plain `sqflite_common_ffi` in `dev_dependencies` is enough when the test95 does not need isolate sharing (see `sqflite-common-ffi-testing`).9697## Examples9899### Database work in a compute isolate sharing the instance100101```dart102import 'package:flutter/foundation.dart' show compute;103import 'package:sqflite_ffi/sqflite_ffi.dart';104105Future<void> _insertInIsolate(String path) async {106 // Same sqflite isolate as the main isolate: same Database instance.107 final db = await sqfliteDatabaseFactoryFfi.openDatabase(108 path,109 options: OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false),110 );111 await db.transaction((txn) async {112 await txn.insert('Test', {'name': 'isolate 1'});113 await txn.insert('Test', {'name': 'isolate 2'});114 });115 // Do not close: the main isolate owns the shared instance.116}117118Future<List<Object?>> run(String path) async {119 final db = await sqfliteDatabaseFactoryFfi.openDatabase(120 path,121 options: OpenDatabaseOptions(122 version: 1,123 onCreate: (db, _) => db.execute(124 'CREATE TABLE Test (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)',125 ),126 rollbackActiveTransactionOnOpen: false,127 ),128 );129 try {130 await Future.wait([131 db.transaction((txn) async {132 await txn.insert('Test', {'name': 'main 1'});133 await Future<void>.delayed(const Duration(milliseconds: 100));134 await txn.insert('Test', {'name': 'main 2'});135 }),136 compute(_insertInIsolate, path),137 ]);138 // The background transaction waited for the main one:139 // main 1, main 2, isolate 1, isolate 2140 return (await db.query('Test', orderBy: 'id')).map((r) => r['name']).toList();141 } finally {142 await db.close();143 }144}145```146147### Forcing ffi even when the native sqflite plugin is present148149```dart150import 'package:flutter/widgets.dart';151import 'package:sqflite_ffi/sqflite_ffi.dart';152153Future<void> main() async {154 WidgetsFlutterBinding.ensureInitialized();155 // Both sqflite and sqflite_ffi are in pubspec.yaml; pick ffi explicitly.156 databaseFactory = sqfliteDatabaseFactoryFfi;157 runApp(const SizedBox());158}159```160161### Database path with path_provider162163```dart164import 'package:path/path.dart' as p;165import 'package:path_provider/path_provider.dart';166import 'package:sqflite_ffi/sqflite_ffi.dart';167168Future<Database> openAppDatabase() async {169 final dir = await getApplicationSupportDirectory();170 return openDatabase(171 p.join(dir.path, 'app.db'),172 version: 1,173 onCreate: (db, _) => db.execute(174 'CREATE TABLE Note (id INTEGER PRIMARY KEY, text TEXT)',175 ),176 );177}178```179180### Isolate spawned outside Flutter181182```dart183import 'dart:isolate';184import 'dart:ui' show DartPluginRegistrant;185186import 'package:sqflite_ffi/sqflite_ffi.dart';187188Future<int> countInIsolate(String path) => Isolate.run(() async {189 // Needed for the global openDatabase(); not for sqfliteDatabaseFactoryFfi.190 DartPluginRegistrant.ensureInitialized();191 final db = await openDatabase(192 path,193 options: OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false),194 );195 final rows = await db.rawQuery('SELECT COUNT(*) AS c FROM Note');196 return rows.first['c'] as int;197 });198```199200### Custom ffiInit201202```dart203import 'package:sqflite_ffi/sqflite_ffi.dart';204205void _ffiInit() {206 // Runs inside the shared sqflite isolate before the first SQLite call.207}208209final myFactory = createSqfliteDatabaseFactoryFfi(ffiInit: _ffiInit);210```211212### Flutter test213214```dart215import 'package:flutter_test/flutter_test.dart';216import 'package:sqflite_ffi/sqflite_ffi.dart';217218void main() {219 TestWidgetsFlutterBinding.ensureInitialized();220 sqfliteFfiInit();221222 test('open in memory', () async {223 final db = await sqfliteDatabaseFactoryFfi.openDatabase(inMemoryDatabasePath);224 expect(await db.getVersion(), 0);225 await db.close();226 });227}228```229230## Common mistakes231232* Depending on `sqflite_ffi` and still calling `sqfliteFfiInit()` +233 `databaseFactory = databaseFactoryFfi` from `sqflite_common_ffi`: this234 creates a second, non-shared sqflite isolate and prints the "changing235 sqflite default factory" warning. Use `sqfliteDatabaseFactoryFfi` or236 nothing.237* Expecting ffi on the web: use `sqflite_common_ffi_web`.238* Closing a shared single-instance database from a background isolate.239* Opening the same file from two isolates in debug mode without240 `rollbackActiveTransactionOnOpen: false`.241* Calling `openDatabase()` in a hand-spawned isolate without242 `DartPluginRegistrant.ensureInitialized()`: `databaseFactory not243 initialized`.244* Having both `sqflite` and `sqflite_ffi` in `pubspec.yaml` and not245 assigning `databaseFactory`: which implementation runs depends on the246 registrant order.