flutter_local_first library
A local-first sync engine for Flutter and Dart.
Local-first means the local database is the source of truth for the UI. Writes complete immediately — offline, on a plane, behind a captive portal — and the engine reconciles with your backend in the background. Users get an app that never blocks on the network; you get eventual consistency without writing the hard parts yourself.
What you provide
- A LocalStore over the database you already use (SQLite, Drift, Isar, Hive...). See InMemoryLocalStore for a reference implementation.
- A RemoteStore over your backend (Firestore, Supabase, REST, GraphQL...). See InMemoryRemoteStore for a testing backend with fault injection.
What you get
- A durable outbox: no local edit is marked synced until the backend accepts it, so nothing is lost when a request dies mid-flight.
- Tombstones so deletions propagate instead of being resurrected by a device that was offline when the delete happened.
- Pluggable conflict resolution — last-write-wins by default, with remote-wins, local-wins, manual, or a custom field-level merge.
- Server-clock cursors so a device with a skewed clock cannot skip another device's writes.
- Attachment syncing with concurrency limits, retry and progress.
- Observability — a state listenable for your UI and an event stream for logs and diagnostics.
Quick start
final engine = LocalFirstSync(
store: MySqliteLocalStore(db),
remote: MyRestRemoteStore(api),
);
await engine.start();
// Instant, offline-safe write:
await engine.write(SyncDocument(
id: 'note-1',
data: {'title': 'Groceries'},
updatedAt: DateTime.now().toUtc(),
));
// Delete safely — travels as a tombstone:
await engine.delete('note-1');
A note on ordering
Push runs before pull inside a cycle. That ordering is what lets the engine advance its cursor safely: by the time it asks "what changed since X?", its own writes are already acknowledged, so it will not pull its own data back.
Classes
- AlwaysOnlineMonitor
- A monitor that is always online. The default when you do not supply one.
- Attachment
- A binary asset that travels alongside its record.
- AttachmentProgress
- Progress for a single attachment transfer.
- AttachmentQueue
- Queues attachment uploads and downloads with retry and progress.
- AttachmentTransport
- The transfer primitives an AttachmentQueue needs from your backend.
- BatchRemoteStore
- An optional extension of RemoteStore for backends that can fetch several records by id in one round trip.
- Conflict
- A conflict detected while pushing a local change that the backend had already moved on from.
- ConflictResolved
- A record changed on both sides and was settled by policy.
- ConnectivityChanged
- Connectivity changed.
- ConnectivityMonitor
- How the engine learns whether the device can reach the backend.
- Failed
- The last sync failed and the engine is waiting to retry.
- Idle
- Nothing to do; the last sync succeeded (or none has run yet).
- InMemoryLocalStore
- An in-memory LocalStore for tests, prototypes and small datasets.
- InMemoryRemoteStore
- An in-memory backend with fault injection, for tests and local development.
- LocalFirstSync
- A local-first sync engine.
- LocalRecordChanged
- A local record was written as the result of a pull or a conflict.
- LocalStore
- The contract the engine needs from your local persistence layer.
- ManualConnectivityMonitor
- A ConnectivityMonitor you drive by hand.
- Offline
- The engine is running but the device is offline.
- ProbeConnectivityMonitor
- A ConnectivityMonitor that treats a probe function as the source of truth.
- PullResult
- A page of remote changes.
- PushResult
- What a push achieved.
- RecordsPulled
- Records arrived from the backend and were written locally.
- RecordsPushed
- Records were accepted by the backend.
- RemoteStore
- The contract the engine needs from your backend.
- RetryPolicy
- Controls how the engine waits between retries after a failed sync.
- Stopped
- The engine has been stopped by the caller.
- StreamConnectivityMonitor
-
A ConnectivityMonitor backed by any
Stream<bool>. -
SyncCodec<
T> - Converts between your domain model and SyncDocument.
- SyncCompleted
- A sync cycle finished successfully.
- SyncConfig
- How the engine should behave: batching, conflict policy, retry and timers.
- SyncDocument
-
A
Map-backed SyncRecord for when you don't want to write a class. - SyncEvent
- Something the engine did, for logging, analytics or a debug console.
- SyncFailed
- A sync cycle failed; a retry may be scheduled.
- Syncing
- A sync is in flight.
- SyncOutcome
- A complete sync cycle's outcome.
- SyncRecord
- The contract every synced record must satisfy.
- SyncStarted
- A sync cycle began.
- SyncState
- What the engine is doing right now.
- SyncStats
- Counters describing what the engine has done.
- TombstonesPurged
- Tombstones older than the configured TTL were removed.
-
ValueListenable<
T> -
Minimal
ValueListenableso the package needs no Flutter dependency.
Enums
- AttachmentState
- Where an attachment is in its lifecycle.
- ConflictPolicy
- How the engine settles a record that changed both locally and remotely.
- ConflictResolution
- The outcome of settling a Conflict.
- LocalChangeReason
- Why a local record changed outside of a direct user write.
- SyncPhase
- The phases of a sync cycle.
Functions
-
resolveConflict(
ConflictPolicy policy, {required SyncDocument local, required SyncDocument remote}) → ConflictResolution - Applies a ConflictPolicy to a local/remote pair and reports the outcome.
-
sameContent(
SyncDocument a, SyncDocument b) → bool - Whether two documents carry the same payload and deletion state, ignoring timestamps and revisions.
-
winnerFor(
ConflictPolicy policy, {required SyncDocument local, required SyncDocument remote}) → SyncDocument -
Returns the winning document for
policy.
Typedefs
- ConflictResolver = SyncDocument Function(SyncDocument local, SyncDocument remote)
- Settles a conflict by returning the winning document.
Exceptions / Errors
- Thrown by InMemoryRemoteStore when configured to fail, and a reasonable base class for transport errors in your own RemoteStore.