dart_data 0.1.1 copy "dart_data: ^0.1.1" to clipboard
dart_data: ^0.1.1 copied to clipboard

The SwiftData for Flutter. Zero-boilerplate, offline-first data persistence and sync framework with SQLite storage, reactive streams, 6 conflict resolution strategies, encryption, and pluggable backen [...]

dart_data #

pub package CI License: BSD-3 Dart 3

The SwiftData for Flutter. Zero-boilerplate, offline-first data persistence and sync for Dart.

Apple gave iOS developers SwiftData — a declarative, model-driven persistence layer that Just Works. Flutter deserved the same. dart_data is that answer.

Annotate your models. Get local SQLite persistence, cloud sync, conflict resolution, encryption, and reactive streams — with near-zero configuration.

@SyncModel(tableName: 'todos')
class Todo {
  @SyncId()
  final String id;

  @SyncField(index: true)
  final String title;

  @SyncField()
  final bool completed;

  Todo({required this.id, required this.title, this.completed = false});
}
// Initialize once
final dd = await DartData.initialize(
  databasePath: 'app.db',
  schemas: [TodoSchema()],
);

// CRUD in one line
final repo = dd.repository<Todo>(TodoSchema());
await repo.save(Todo(id: '', title: 'Buy milk'));

// Reactive UI
repo.watchAll().listen((todos) => updateUI(todos));

// Sync when ready
await syncEngine.sync(); // push local, pull remote, resolve conflicts

That's it. No boilerplate. No manual SQL. No sync plumbing.


Why dart_data? #

What you get SwiftData dart_data
Declarative models @Model @SyncModel
Auto-persistence Core Data + SQLite SQLite with auto-schema
Cloud sync CloudKit REST, Firebase, Supabase
Conflict resolution Last-write-wins 6 strategies (LWW, field-merge, 3-way merge, ...)
Reactive queries @Query watchAll() / watch() streams
Encryption Data Protection SQLCipher with key rotation
Code generation Swift macros build_runner + annotations
Platform Apple only Everywhere Dart runs

dart_data brings the SwiftData developer experience to every platform — mobile, web, desktop, and server — with a sync engine that goes far beyond what CloudKit offers.


Features #

  • Offline-first architecture — Writes go to local SQLite instantly. Sync happens in the background. Your app never blocks on the network.
  • Zero-boilerplate models — Annotate with @SyncModel, run build_runner, get typed schemas, repositories, and migrations generated for you.
  • Full CRUD + reactive streamssave, findById, findAll, delete, watch, watchAll, batch operations, soft deletes.
  • Sync engine with operation queue — Persistent queue survives app restarts. FIFO with smart prioritization. Exponential backoff. Dead-letter pool.
  • 6 conflict resolution strategies — Last-write-wins, server-wins, client-wins, field-level merge, three-way merge, or fully manual.
  • Hybrid Logical Clock — Distributed causal ordering that works even when device clocks disagree. Based on Kulkarni et al. 2014.
  • Pluggable backend adapters — REST, Firebase Firestore, and Supabase out of the box. Implement BackendAdapter for anything else.
  • SQLCipher encryption — AES-256 encryption at rest with key rotation support.
  • Auto schema migration — Detects column additions, type changes, index changes. Generates diffs. Supports custom migration steps.
  • Pure Dart — No Flutter dependency. Works in CLI tools, backend services, and Flutter apps alike.

Installation #

dependencies:
  dart_data: ^0.1.0

For code generation:

dev_dependencies:
  dart_data_generator: ^0.1.0
  build_runner: ^2.4.0

Pick your backend:

dependencies:
  dart_data_rest: ^0.1.0       # Any REST API
  dart_data_firebase: ^0.1.0   # Firebase Firestore
  dart_data_supabase: ^0.1.0   # Supabase

Quick Start #

1. Define your model #

import 'package:dart_data/dart_data.dart';

@SyncModel(tableName: 'todos')
class Todo {
  @SyncId()
  final String id;

  @SyncField(index: true)
  final String title;

  @SyncField()
  final bool completed;

  @SyncField(nullable: true, columnName: 'due_date')
  final DateTime? dueDate;

  Todo({required this.id, required this.title, this.completed = false, this.dueDate});
}

2. Generate the schema #

dart run build_runner build

3. Initialize and use #

final dd = await DartData.initialize(
  databasePath: 'app.db',
  schemas: [TodoSchema()],
);

final todos = dd.repository<Todo>(TodoSchema());

// Create
final todo = await todos.save(Todo(id: '', title: 'Ship dart_data'));

// Read
final all = await todos.findAll(
  where: QueryBuilder().where('completed', equals: 0),
  orderBy: OrderBy('_created_at', descending: true),
  limit: 20,
);

// Update
await todos.save(todo.copyWith(completed: true));

// Delete (soft by default, hard optional)
await todos.delete(todo);
await todos.delete(todo, hard: true);

// Reactive streams
todos.watchAll().listen((list) => print('${list.length} todos'));
todos.watch('some-id').listen((todo) => print('Changed: $todo'));

// Batch operations (atomic)
await todos.batch((ctx) async {
  await ctx.save(Todo(id: '', title: 'Task 1'));
  await ctx.save(Todo(id: '', title: 'Task 2'));
});

Sync Engine #

The sync engine provides offline-first synchronization between local storage and any remote backend.

final syncEngine = SyncEngine(
  adapter: RestAdapter(),
  queue: OperationQueue(storage: sqliteStorage),
  statusNotifier: SyncStatusNotifier(),
  modelTypes: ['Todo', 'User'],
  defaultConflictStrategy: ConflictStrategy.lastWriteWins,
);

// Full sync cycle
final result = await syncEngine.sync();
print('Pushed: ${result.pushSummary.accepted} accepted');
print('Pulled: ${result.pullSummaries.map((p) => p.applied).toList()}');

How it works #

LOCAL WRITE
  1. Write to SQLite immediately (instant UI update)
  2. Enqueue SyncOperation
  3. Online? Push now. Offline? Queue persists until connectivity returns.

PUSH (upload)
  1. Dequeue pending operations in batch
  2. Send to backend → accepted / rejected / conflicted per operation
  3. Resolve conflicts with configured strategy

PULL (download)
  1. Fetch remote changes since last sync timestamp
  2. No local conflict → apply directly
  3. Local conflict → resolve with configured strategy
  4. Update sync cursor

Conflict Resolution #

Six built-in strategies cover every scenario:

Strategy Use case
Last Write Wins General purpose. Most recent HLC timestamp wins.
Server Wins Admin-controlled data. Server is always authoritative.
Client Wins Single-user data. Local edits always take precedence.
Field-Level Merge Collaborative editing. Non-conflicting fields merge independently.
Three-Way Merge Precision merging using the common ancestor as base.
Manual Surface conflicts to the user via callback or UI.

Configure per-model or globally:

final syncEngine = SyncEngine(
  adapter: adapter,
  queue: queue,
  statusNotifier: statusNotifier,
  defaultConflictStrategy: ConflictStrategy.lastWriteWins,
  modelConflictStrategies: {
    'Todo': ConflictStrategy.fieldLevelMerge,
    'User': ConflictStrategy.serverWins,
  },
);

Encryption #

Encrypt your database at rest with SQLCipher:

final dd = await DartData.initialize(
  databasePath: 'encrypted.db',
  schemas: [TodoSchema()],
  encryptionConfig: EncryptionConfig(
    key: 'my-secret-key',
    kdfIterations: 256000,
    pageSize: CipherPageSize.size4096,
  ),
);

// Key rotation
final storage = dd.storage as SqliteStorage;
await storage.rotateKey(EncryptionConfig(key: 'new-secret-key'));

Schema Migration #

Auto-detect and apply schema changes between versions:

final dd = await DartData.initialize(
  databasePath: 'app.db',
  schemas: [TodoSchemaV2()],
  migrationConfig: MigrationConfig(
    schemaVersion: 2,
    autoMigrate: true,
    migrations: [
      MigrationStep(
        version: 2,
        description: 'Add priority column',
        up: (db) async {
          db.execute('UPDATE todos SET priority = 0 WHERE priority IS NULL');
        },
      ),
    ],
  ),
);

Auto-migration handles: adding columns, removing columns, changing column types, changing nullability, adding/removing indexes, and full table rebuilds when needed.


Backend Adapters #

REST #

import 'package:dart_data_rest/dart_data_rest.dart';

final adapter = RestAdapter();
await adapter.initialize(BackendConfig(
  url: 'https://api.example.com',
  headers: {'Authorization': 'Bearer token'},
));

Firebase Firestore #

import 'package:dart_data_firebase/dart_data_firebase.dart';

final adapter = FirebaseAdapter(projectId: 'my-project');
await adapter.initialize(BackendConfig(
  url: 'https://firestore.googleapis.com',
));

Supabase #

import 'package:dart_data_supabase/dart_data_supabase.dart';

final adapter = SupabaseAdapter(supabaseUrl: 'https://xyz.supabase.co');
await adapter.initialize(BackendConfig(
  headers: {'apikey': 'your-anon-key'},
));

Custom #

Implement BackendAdapter to connect to any backend:

class MyAdapter implements BackendAdapter {
  @override
  Future<void> initialize(BackendConfig config) async { /* ... */ }

  @override
  Future<List<PushResult>> push(List<SyncOperation> operations) async { /* ... */ }

  @override
  Future<PullResult> pull(String modelType, {DateTime? since, int? limit}) async { /* ... */ }

  // ...
}

Architecture #

dart_data (core)
  ├── Storage Engine ......... SQLite with auto-schema, metadata tracking
  ├── Repository ............. Type-safe CRUD, queries, reactive streams, batch ops
  ├── Sync Engine ............ Push/pull orchestration, operation queue, connectivity
  ├── Conflict Resolution .... 6 strategies, Hybrid Logical Clock ordering
  ├── Migration Engine ....... Auto-diff, custom steps, table rebuild
  └── Encryption ............. SQLCipher AES-256, key rotation

dart_data_generator (codegen)
  └── @SyncModel → ModelSchema, toJson/fromJson, typed repositories

dart_data_rest        ─┐
dart_data_firebase     ├── Backend adapters (implement BackendAdapter)
dart_data_supabase    ─┘

Packages #

Package Description pub.dev
dart_data Core framework pub
dart_data_generator Code generation for models pub
dart_data_rest REST backend adapter pub
dart_data_firebase Firebase Firestore adapter pub
dart_data_supabase Supabase adapter pub

Contributing #

Contributions are welcome! Please file issues and pull requests on GitHub.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Run tests (dart test in any package directory)
  4. Submit a pull request

See CONTRIBUTING.md for details.


License #

BSD 3-Clause. See LICENSE.

1
likes
130
points
61
downloads

Publisher

unverified uploader

Weekly Downloads

The SwiftData for Flutter. Zero-boilerplate, offline-first data persistence and sync framework with SQLite storage, reactive streams, 6 conflict resolution strategies, encryption, and pluggable backend adapters.

Topics

#database #sqlite #offline-first #sync #persistence

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

drift, meta, sqlite3, uuid

More

Packages that depend on dart_data