Agent Skills › tekartik/sqflite › sqflite-ffi-flutter

sqflite-ffi-flutter

GitHub

提供 sqflite_ffi 在 Flutter 跨平台(桌面/移动)的 FFI SQLite 集成指南,涵盖插件注册、多 Isolate 共享数据库及路径初始化配置。

packages_flutter/sqflite_ffi/skills/sqflite-ffi-flutter/SKILL.md tekartik/sqflite

Trigger Scenarios

Flutter 应用需要跨平台使用 SQLite 配置 sqflite_ffi 数据库路径或隔离共享

Install

npx skills add tekartik/sqflite --skill sqflite-ffi-flutter -g -y
More Options

Non-standard path

npx skills add https://github.com/tekartik/sqflite/tree/master/packages_flutter/sqflite_ffi/skills/sqflite-ffi-flutter -g -y

Use without installing

npx skills use tekartik/sqflite@sqflite-ffi-flutter

指定 Agent (Claude Code)

npx skills add tekartik/sqflite --skill sqflite-ffi-flutter -a claude-code -g -y

安装 repo 全部 skill

npx skills add tekartik/sqflite --all -g -y

预览 repo 内 skill

npx skills add tekartik/sqflite --list

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 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 and getDatabasesPath() resolve under .dart_tool/sqflite_common_ffi/databases in the current directory, which is / (not writable) on Android and iOS. sqflite_ffi does not depend on path_provider: in an app, call databaseFactory.setDatabasesPath(...) once in main() (not on web) with a path_provider folder: a databases folder in getApplicationSupportDirectory() on every platform, or the native sqflite folders (Android <data>/databases, iOS and macOS Documents) to keep the databases of an app moving from sqflite. 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 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());
}

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_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.
  • 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 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.

Version History

  • 52dbf98 Current 2026-11-02 13:06

    新增关于数据库路径初始化的文档,说明如何在 Android/iOS 上正确设置可写路径及应用支持目录方案。

  • aaabf90 2026-09-22 03:38

Same Skill Collection

packages/sqflite_common_ffi_async_test/skills/sqflite-common-ffi-async-test-suite/SKILL.md
packages/sqflite_common_ffi_async/skills/sqflite-common-ffi-async-factory/SKILL.md
sqflite_android/skills/sqflite-android-setup/SKILL.md
sqflite_common_ffi/skills/sqflite-common-ffi-testing/SKILL.md
packages_web/sqflite_common_ffi_web_test/skills/sqflite-common-ffi-web-test-suite/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-options/SKILL.md
packages_web/sqflite_common_ffi_web/skills/sqflite-common-ffi-web-setup/SKILL.md
sqflite_common_ffi/skills/sqflite-common-ffi-desktop/SKILL.md
sqflite_common_test/skills/sqflite-common-test-suite/SKILL.md
sqflite_common/skills/sqflite-common-api/SKILL.md
sqflite_darwin/skills/sqflite-darwin-setup/SKILL.md
sqflite_platform_interface/skills/sqflite-platform-interface-implementers/SKILL.md
sqflite/skills/sqflite-crud-and-transactions/SKILL.md
sqflite/skills/sqflite-open-database/SKILL.md
sqflite/skills/sqflite-testing-and-platforms/SKILL.md

Metadata

Files
0
Version
52dbf98
Hash
7e7bab8c
Indexed
2026-09-22 03:38

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.156.0. UTC+08:00, 2026-11-06 06:16
浙ICP备14020137号-1