Agent Skills
› tekartik/sqflite
› sqflite-ffi-flutter
sqflite-ffi-flutter
GitHub介绍 sqflite_ffi Flutter 插件,用于在桌面和移动端通过 FFI 实现 SQLite。支持多 Isolate 共享、自动注册及与原生插件共存,提供跨平台数据库访问方案。
Trigger Scenarios
Flutter 应用需要跨平台(含桌面)SQLite 支持
需要在多个 Dart Isolate 中共享数据库连接
选择 sqflite_ffi 与其他 SQLite 包的差异
Install
npx skills add tekartik/sqflite --skill sqflite-ffi-flutter -g -y
SKILL.md
Frontmatter
{
"name": "sqflite-ffi-flutter",
"description": "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."
}
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 runssqfliteFfiInit()and setssqfliteDatabaseFactoryFfias the globaldatabaseFactoryif none is registered yet. The globalopenDatabase()works on Windows, Linux, macOS, Android and iOS with nomain()code. - All Flutter isolates (main,
compute,Isolate.run) share one sqflite isolate: itsSendPortis registered inIsolateNameServerundersqfliteFfiIsolatePortName, sosingleInstanceand 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) todependencies. It bringssqflite_common_ffiandsqlite3(>= 3, build hooks: SQLite is bundled, nothing to install; runflutter cleanwhen changing thesqlite3major version). - Import
package:sqflite_ffi/sqflite_ffi.dart. It re-exportssqflite_common_ffi/sqflite_ffi.dart(hence the wholesqflite_commonAPI:Database,openDatabase,deleteDatabase,inMemoryDatabasePath,databaseFactory,sqfliteFfiInit,SqfliteFfiInit,databaseFactoryFfiNoIsolate,SqfliteFfiIsolatePortServer) but hidesdatabaseFactoryFfiandcreateDatabaseFactoryFfi. Use the package's ownsqfliteDatabaseFactoryFfiandcreateSqfliteDatabaseFactoryFfiinstead. - Choosing between packages:
sqflitealone: native plugin, iOS/Android/macOS only.sqflite_common_ffi: pure Dart, desktop and tests; you setdatabaseFactoryyourself; 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_webfor the web:sqflite_ffiis a no-op there (SqfliteFfiPlugin.registerWith()does nothing andsqfliteDatabaseFactoryFfireturns the unsupported ffi factory).
- Registration order:
SqfliteFfiPlugin.registerWith()usesdatabaseFactoryOrNull ??=, and so does the nativesqfliteplugin (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 inmain():databaseFactory = sqfliteDatabaseFactoryFfi;to force ffi, ordatabaseFactory = databaseFactorySqflitePlugin;(exported bypackage: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 throughIsolateNameServer.ffiInitmust be top-level or static and runs in the sqflite isolate before the first SQLite call; the native library itself is selected by thesqlite3build hook user defines inpubspec.yaml(hooks.user_defines.sqlite3.source), seesqflite-common-ffi-desktop.- Do not assume plugin registration in background isolates (
compute,Isolate.run,Isolate.spawn): the globaldatabaseFactorymay be unset there. Either callDartPluginRegistrant.ensureInitialized()(fromdart:ui) before the globalopenDatabase(), or usesqfliteDatabaseFactoryFfidirectly: theIsolateNameServerlookup works in any isolate without registration. - When two isolates open the same file, pass
OpenDatabaseOptions(rollbackActiveTransactionOnOpen: false)(the default istruein debug mode) so the secondopenDatabasedoes not roll back a transaction running in the first isolate. Do notclose()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/databasesin the current directory. In an app usepath_provider(getApplicationSupportDirectory()) andpackage:pathjoin. The parent directory is created on open. - Tests:
flutter testworks withsqfliteDatabaseFactoryFfiafterTestWidgetsFlutterBinding.ensureInitialized()andsqfliteFfiInit(); plainsqflite_common_ffiindev_dependenciesis enough when the test does not need isolate sharing (seesqflite-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_ffiand still callingsqfliteFfiInit()+databaseFactory = databaseFactoryFfifromsqflite_common_ffi: this creates a second, non-shared sqflite isolate and prints the "changing sqflite default factory" warning. UsesqfliteDatabaseFactoryFfior 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 withoutDartPluginRegistrant.ensureInitialized():databaseFactory not initialized. - Having both
sqfliteandsqflite_ffiinpubspec.yamland not assigningdatabaseFactory: which implementation runs depends on the registrant order.
Version History
- aaabf90 Current 2026-09-22 03:38


