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

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 ValueListenable so 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

RemoteUnavailable
Thrown by InMemoryRemoteStore when configured to fail, and a reasonable base class for transport errors in your own RemoteStore.