Agent Skills
› tekartik/sqflite
› sqflite-ffi-flutter
sqflite-ffi-flutter
GitHub提供 sqflite_ffi 在 Flutter 跨平台(桌面/移动)的 FFI SQLite 集成指南,涵盖插件注册、多 Isolate 共享数据库及路径初始化配置。
触发场景
Flutter 应用需要跨平台使用 SQLite
配置 sqflite_ffi 数据库路径或隔离共享
安装
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 and
getDatabasesPath()resolve under.dart_tool/sqflite_common_ffi/databasesin the current directory, which is/(not writable) on Android and iOS.sqflite_ffidoes not depend onpath_provider: in an app, calldatabaseFactory.setDatabasesPath(...)once inmain()(not on web) with apath_providerfolder: adatabasesfolder ingetApplicationSupportDirectory()on every platform, or the native sqflite folders (Android<data>/databases, iOS and macOS Documents) to keep the databases of an app moving fromsqflite. The path is stored in the factory of the calling isolate: pass full paths to background isolates. 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());
}
Databases path with path_provider (all platforms)
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_ffi/sqflite_ffi.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb) {
final dir = await getApplicationSupportDirectory();
await databaseFactory.setDatabasesPath(p.join(dir.path, 'databases'));
}
// Relative paths now resolve in the application support directory.
final db = await openDatabase(
'app.db',
version: 1,
onCreate: (db, _) => db.execute(
'CREATE TABLE Note (id INTEGER PRIMARY KEY, text TEXT)',
),
);
await db.close();
runApp(const SizedBox());
}
Same folders as the native sqflite plugin
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
Future<String> sqflitePluginDatabasesPath() async {
if (Platform.isAndroid) {
// <data>/files and <data>/databases share the application data directory.
final dir = await getApplicationSupportDirectory();
return p.join(p.dirname(dir.path), 'databases');
} else if (Platform.isIOS || Platform.isMacOS) {
return (await getApplicationDocumentsDirectory()).path;
}
// Linux, Windows: no native plugin.
return p.join((await getApplicationSupportDirectory()).path, 'databases');
}
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. - Opening a relative path on Android or iOS without
databaseFactory.setDatabasesPath(...): the default folder is under/, not writable. - 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.
版本历史
-
52dbf98
当前 2026-11-02 13:06
新增关于数据库路径初始化的文档,说明如何在 Android/iOS 上正确设置可写路径及应用支持目录方案。
- aaabf90 2026-09-22 03:38


