flowdb 1.0.0
flowdb: ^1.0.0 copied to clipboard
A lightweight file-based local database for Dart and Flutter with collections, key-value stores, blob storage, queries, backups, and reactive FlowState.
flowdb #
A lightweight, file-based local database for Dart and Flutter. flowdb stores data on disk as JSON, with collections for structured records, key-value stores, chunked blob storage, backups, and optional encryption. It also includes reactive state (FlowState) and a Flutter widget (FlowBuilder) for stream-driven UIs.
Features #
- File-based persistence — no native drivers or SQL; data lives under a database directory on the filesystem
- Collections — document-style records with auto-generated CUID ids, CRUD, and rich querying
- Key-value stores — simple
get/set/removestorage scoped to the database - Blob storage — large binary files stored in configurable chunks with metadata in a collection
- Query builder — filter, sort, paginate, project fields, and run bulk updates/deletes
- Relations — define one-to-one, one-to-many, and many-to-many links between collections
- Backups — snapshot the entire database directory
- Optional encryption — encrypt collection and store payloads at rest
- Sync and async APIs — most operations offer both
foo()andfooSync()variants - Reactive state —
FlowState<T>built on RxDartBehaviorSubject - Flutter integration —
FlowBuilderconnectsFlowStatestreams to widgets
Requirements #
- Dart SDK
^3.10.7 - Flutter
>=1.17.0(only if you usepackage:flowdb/flutter.dart)
Installation #
Add flowdb to your pubspec.yaml:
dependencies:
flowdb: ^1.0.0
For Dart-only projects, import the core library:
import 'package:flowdb/core.dart';
For Flutter apps that need FlowBuilder, import:
import 'package:flowdb/flutter.dart';
Quick start #
import 'package:flowdb/core.dart';
Future<void> main() async {
final db = openDatabase('my_app', path: './data/my_app');
final users = db.collection('users');
final alice = await users.add({'name': 'Alice', 'age': 30});
print(alice.id); // auto-generated CUID
final found = await users.where('name', eq: 'Alice').getFirst();
print(found?.data);
final settings = db.store('settings');
settings.set('theme', 'dark');
print(settings.get('theme'));
}
Core concepts #
Database #
Open a database with openDatabase. All collections, stores, blobs, and backups live under the database path.
final db = openDatabase(
'my_app',
path: './data/my_app', // optional; defaults to the database name
encrypted: false, // encrypt collection/store file contents
isWeb: false, // web platform hint
);
| Method / property | Description |
|---|---|
collection(name) |
Get or create a collection |
store(name) |
Get or create a key-value store |
blobStorage(name) |
Get or create chunked blob storage |
relation(...) |
Define a relation between collections |
backup(name) |
Create a filesystem backup |
reset() / clear() |
Delete and recreate the database directory |
drop() |
Delete the database directory entirely |
size / sizeFormatted |
Total on-disk size |
Collections and records #
A collection holds records — maps of fields keyed by string, each with a stable id.
final posts = db.collection('posts');
// Create
final record = await posts.add({'title': 'Hello', 'published': true});
final batch = await posts.addMany([
{'title': 'First'},
{'title': 'Second'},
]);
// Read by id
final one = await posts.getById(record.id);
// Read all
final all = await posts.getAll();
final total = await posts.count;
Records support nested field access via dot notation:
record.set('author.name', 'Alice');
final authorName = record.get('author.name');
Export a collection as CSV:
final csv = await posts.exportAsCSV();
Querying #
Chain filters on collection.where(...), collection.builder, or the query returned by where:
final results = await posts
.where('published', eq: true)
.and('views', gte: 100)
.orderBy('title')
.skip(10)
.limit(20)
.get();
final first = await posts.where('title', startsWith: 'Hello').getFirst();
final matching = await posts.where('tags', contains: 'dart').get();
Comparison operators
| Parameter | Meaning |
|---|---|
eq / neq |
Equal / not equal |
gt / gte / lt / lte |
Numeric ordering |
contains / doesNotContain |
Substring or list membership |
startsWith / endsWith |
String prefix / suffix |
regexMatch |
Regular expression |
between, betweenStartInclusive, betweenEndInclusive, betweenBothInclusive |
Range checks |
isNull / isNotNull |
Null checks |
isEmpty / isNotEmpty |
Empty string or collection |
filter |
Custom bool Function(dynamic value) |
Other query methods
| Method | Description |
|---|---|
or(...) |
OR condition (after an initial where) |
pick / pickMany |
Include only selected fields |
omit / omitMany |
Exclude fields from results |
orderBy(field, desc: true) |
Sort results |
skip / limit / take |
Pagination |
include / includeMany |
Load related data (when relations are configured) |
getShuffled / getRandom / getRandomMany |
Random access |
update / updateMany |
Update matching records |
delete / deleteMany |
Delete matching records |
upsert |
Update if matched, otherwise insert |
addIfAbsent |
Insert only when no match |
Sync variants mirror the async API: getSync(), addSync(), countSync, and so on.
Key-value stores #
Stores hold arbitrary JSON-serializable values under string keys.
final cache = db.store('cache');
cache.set('lastSync', DateTime.now().toIso8601String());
cache.setFrom('counter', (v) => (v as int? ?? 0) + 1, 0);
if (cache.has('lastSync')) {
print(cache.get('lastSync'));
}
final all = cache.all(); // Map of all entries
cache.clear();
cache.drop(); // delete the store from disk
Blob storage #
Store large binary data in chunked files. Metadata is kept in an internal collection; bytes are written in configurable chunk sizes (default 512 KB).
import 'dart:typed_data';
final files = db.blobStorage('files', chunksSize: 1024 * 512);
await files.add(
metadata: {'filename': 'photo.png', 'mime': 'image/png'},
bytes: Uint8List.fromList([/* ... */]),
);
final blob = await files.get((meta) => meta.where('filename', eq: 'photo.png'));
final bytes = await blob?.bytes;
Relations #
Define how collections link to each other. Relations are registered on the database and can be used with query include.
db
.relation('users', 'id', name: 'posts')
.hasMany('posts', 'authorId');
// one-to-one
db.relation('users', 'profileId', name: 'profile').hasOne('profiles', 'id');
// many-to-many
db.relation('users', 'id', name: 'tags').manyToMany('tags', 'id');
Backups #
Backups copy the database directory (excluding existing backup folders) into backups/<name>/.
final snapshot = db.backup('before-migration');
print(db.backups); // list of Backup instances
db.removeBackup('before-migration');
db.removeAllBackups();
Reactive state (FlowState) #
FlowState<T> wraps a seeded BehaviorSubject for synchronous reads and stream updates.
final counter = FlowState<int>(0);
counter.listen((value) => print('count: $value'));
counter.update(1);
counter.updateFrom((v) => v + 1);
final combined = FlowState.combine2(
counter,
FlowState<String>(''),
(count, label) => '$label: $count',
);
await counter.close();
FlowState.combine2 through combine9, plus FlowState.list, merge multiple states into one.
Flutter integration #
Use FlowBuilder to rebuild widgets when a FlowState changes:
import 'package:flowdb/flutter.dart';
import 'package:flutter/material.dart';
class CounterView extends StatelessWidget {
const CounterView({super.key, required this.counter});
final FlowState<int> counter;
@override
Widget build(BuildContext context) {
return FlowBuilder<int>(
flow: counter,
builder: (value) => Text('Count: $value'),
onLoadingBuilder: () => const CircularProgressIndicator(),
onErrorBuilder: (e) => Text('Error: $e'),
onNoDataBuilder: () => const Text('No data'),
);
}
}
Utilities #
The core library also exports helpers used internally and available for app code:
| Export | Purpose |
|---|---|
BGWorker |
Run a function in a separate isolate |
Json |
JSON encode/decode, nested get/update, CSV conversion |
Encryptor |
Encrypt/decrypt strings for at-rest storage |
Format |
Human-readable byte sizes |
Performance |
Timing utilities |
PrintColored |
ANSI-colored console output |
Random |
ID generation (CUID) |
On-disk layout #
A database directory typically looks like:
my_app/
├── collections/
│ └── users # newline-delimited JSON records
├── stores/
│ └── settings # JSON key-value file
├── blobs/
│ └── files/
│ ├── _metadata/ # blob metadata collection
│ └── chunks/ # binary chunks
└── backups/
└── snapshot_name/
Sync vs async #
Most collection and query methods exist in pairs:
add/addSyncgetAll/getAllSyncwhere(...).get()/where(...).getSync()
Use async methods in UI code and isolates where blocking I/O is undesirable. Use sync methods in scripts, tests, or startup paths where simplicity matters.
Encryption #
Pass encrypted: true when opening a database to encrypt collection and store file contents. Encrypted databases propagate the setting to collections and stores created through db.collection() and db.store().
Additional information #
- Version: 1.0.0
- License: MIT
- Repository: github.com/kidusab/flowdb