Skip to content

Dart/Flutter Bindings

DecentDB provides Dart FFI bindings for Flutter desktop apps under bindings/dart/.

Build the native library

nimble build_lib

On Linux this produces build/libc_api.so, on macOS build/libc_api.dylib, on Windows build/c_api.dll.

Installation

Add the decentdb package to your pubspec.yaml:

dependencies:
  decentdb:
    path: <path-to-repo>/bindings/dart/dart

Quick Start

import 'package:decentdb/decentdb.dart';

void main() {
  final db = Database.open('mydata.ddb', libraryPath: 'path/to/libc_api.so');

  db.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
  db.execute("INSERT INTO users VALUES (1, 'Alice')");

  final rows = db.query('SELECT * FROM users');
  for (final row in rows) {
    print('${row["id"]}: ${row["name"]}');
  }

  db.close();
}

Prepared Statements

final stmt = db.prepare(r'INSERT INTO users VALUES ($1, $2)');
stmt.bindInt64(1, 42);
stmt.bindText(2, 'Bob');
stmt.execute();

// Reuse the statement
stmt.reset();
stmt.clearBindings();
stmt.bindAll([43, 'Charlie']);
stmt.execute();
stmt.dispose();

Cursor Paging

Stream large results page by page without loading everything into memory:

final stmt = db.prepare('SELECT * FROM large_table ORDER BY id');
while (true) {
  final page = stmt.nextPage(100);  // 100 rows per page
  for (final row in page.rows) {
    print(row['name']);
  }
  if (page.isLast) break;
}
stmt.dispose();

Transactions

// Manual
db.begin();
db.execute("INSERT INTO users VALUES (1, 'Alice')");
db.commit();

// Automatic (rolls back on exception)
db.transaction(() {
  db.execute("INSERT INTO users VALUES (2, 'Bob')");
  db.execute("INSERT INTO users VALUES (3, 'Charlie')");
});

Schema Introspection

// Tables
List<String> tables = db.schema.listTables();

// Column metadata
List<ColumnInfo> cols = db.schema.getTableColumns('users');
for (final col in cols) {
  print('${col.name} ${col.type} notNull=${col.notNull} pk=${col.primaryKey}');
}

// Indexes
List<IndexInfo> indexes = db.schema.listIndexes();

// Views
List<String> views = db.schema.listViews();
String? ddl = db.schema.getViewDdl('my_view');

Supported Types

Dart Type Bind Method DecentDB Type
null bindNull() NULL
int bindInt64() INTEGER
bool bindBool() BOOLEAN
double bindFloat64() FLOAT
String bindText() TEXT
Uint8List bindBlob() BLOB
DateTime bindDateTime() TIMESTAMP
decimal bindDecimal() DECIMAL

EXPLAIN

final rows = db.query('EXPLAIN SELECT * FROM users WHERE id = 1');
for (final row in rows) {
  print(row['query_plan']);
}

Error Handling

All errors are thrown as DecentDbException with a structured error code and message:

try {
  db.execute('INVALID SQL');
} on DecentDbException catch (e) {
  print('Error ${e.code}: ${e.message}');
  // e.code is an ErrorCode enum: io, corruption, constraint, transaction, sql, internal
}

Flutter Desktop Integration

Bundling the native library

Linux — copy libc_api.so to linux/libs/ and add to CMakeLists.txt:

install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/libs/libc_api.so"
        DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
        COMPONENT Runtime)

macOS — copy libc_api.dylib to macos/libs/ and add to Xcode "Copy Bundle Resources".

Windows — copy c_api.dll to windows/libs/ and add to CMakeLists.txt:

install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/libs/c_api.dll"
        DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
        COMPONENT Runtime)

Resolving the library path at runtime

import 'dart:io' show Platform;
import 'package:path/path.dart' as p;

String resolveLibPath() {
  final exeDir = p.dirname(Platform.resolvedExecutable);
  if (Platform.isLinux) return p.join(exeDir, 'lib', 'libc_api.so');
  if (Platform.isMacOS) return p.join(exeDir, '..', 'Frameworks', 'libc_api.dylib');
  if (Platform.isWindows) return p.join(exeDir, 'c_api.dll');
  throw UnsupportedError('Unsupported platform');
}

Threading & Isolates

DecentDB uses a single-writer, multiple-reader model:

  • All write operations must be serialized on one isolate
  • Read queries (SELECT) can run concurrently via separate statement handles
  • Each Statement handle must be used from one isolate only

For Flutter apps, run database operations in a dedicated isolate:

void dbWorker(SendPort sendPort) {
  final db = Database.open('app.ddb', libraryPath: libPath);
  final port = ReceivePort();
  sendPort.send(port.sendPort);

  port.listen((message) {
    try {
      final result = db.query(message['sql'], message['params'] ?? []);
      message['replyPort'].send({'rows': result.map((r) => r.values).toList()});
    } on DecentDbException catch (e) {
      message['replyPort'].send({'error': e.toString()});
    }
  });
}

Running Tests

nimble build_lib
bindings/dart/scripts/run_tests.sh

Source Code

The binding source is at bindings/dart/. See the README for full details.