Sync Lifecycle
The sync lifecycle describes how data flows through Locorda - from local changes to remote sync, how offline changes are queued, and how errors are handled.
Overview
Section titled “Overview”Sync in Locorda is automatic and bidirectional:
- Local → Remote: Your changes are queued and uploaded when connected
- Remote → Local: Remote changes are downloaded and merged automatically
Both directions flow through the same callback pattern, ensuring your UI always shows the merged state.
Local Changes Flow
Section titled “Local Changes Flow”When you make a local change (repository.save(task)):
1. 📱 Your app calls repository.save(task) ↓ (repository forwards to syncEngine.save<Task>(task))2. 🔧 SyncEngine merges & stores in Locorda's local storage ↓3. 📞 onUpdate callback (registered via hydrateWithCallbacks) → your app saves to its own database ↓4. 🎨 UI updates via StreamBuilder ↓ [Change is queued for sync when connected]Key insight: You don’t directly update your database in save(). The sync engine calls your onUpdate callback (which you registered with hydrateWithCallbacks() during initialization), which updates your database. This keeps everything consistent.
Why This Pattern?
Section titled “Why This Pattern?”The callback pattern ensures:
- Consistency: Local and remote changes handled identically
- Conflict-free: CRDT merge happens before callback
- Single source of truth: Only one code path for database updates
- Testability: Easy to mock and test
Background Sync Flow
Section titled “Background Sync Flow”Sync process runs automatically in a background worker thread:
1. 🔄 Worker checks remote storage for changes ↓2. 📥 Downloads new/updated data ↓3. 🔧 SyncEngine merges with local storage (resolves conflicts) ↓4. 📤 Uploads merged state + queued local changes to remote ↓5. 📞 onUpdate callback → your app saves to its database ↓6. 🎨 UI updates via StreamBuilderKey insight: Remote changes go through the same onUpdate callback as local changes. Your database and UI don’t need to know where changes came from.
Sync Triggers
Section titled “Sync Triggers”Sync can be triggered by:
- Automatic periodic sync (default behavior)
- Manual sync request (user presses sync button)
Offline Mode
Section titled “Offline Mode”Locorda is offline-first - your app works fully offline, and changes sync when you reconnect.
Offline Behavior
Section titled “Offline Behavior”When offline:
- Local changes are queued: Saved to Locorda’s local storage
- UI updates immediately: Via
onUpdatecallbacks - No remote sync: Worker waits for connection
- No data loss: All changes persisted locally
When you reconnect:
- Sync process runs automatically
- Downloads remote changes (what others did while you were offline)
- Merges with your queued local changes (using CRDT algorithms)
- Both remote and local changes flow through
onUpdate - UI shows the final merged state
Conflict Resolution During Sync
Section titled “Conflict Resolution During Sync”When offline changes from multiple devices are merged:
Device A (offline): Device B (offline):task.title = "Buy milk" task.title = "Buy bread" ↓ ↓ Both reconnect ↓Sync merges using CRDT rules (for example last-write-wins by timestamp) ↓Final merged state: task.title = "Buy bread" (if B's timestamp is later)See Conflict Resolution for details on merge strategies.
Sync States
Section titled “Sync States”SyncStatus
Section titled “SyncStatus”Your app can be in one of these sync states:
| State | Description | User Can |
|---|---|---|
| idle | No sync in progress, waiting for trigger | Make local changes (queued) |
| syncing | Actively syncing data | Everything (sync in background) |
| success | Last sync completed successfully | Everything |
| error | Last sync failed with an error | Retry or continue offline |
SyncTrigger
Section titled “SyncTrigger”Sync operations can be triggered by:
- manual - User explicitly requested sync (e.g., “Sync Now” button)
- startup - Automatic sync on application startup
- scheduled - Automatic sync on a scheduled interval
- pullToRefresh - Sync triggered by pull-to-refresh gesture
- login - Sync triggered after user authentication/login
- dataChange - (planned, not yet implemented) - Sync triggered after local data changes
- connectionRestore (planned, not yet implemented) - Sync after network connectivity is restored
Monitoring Sync State
Section titled “Monitoring Sync State”Subscribe to sync state changes:
// Access sync manager from your Locorda instancefinal syncManager = locorda.syncManager;
// Listen to state changessyncManager.statusStream.listen((state) { switch (state.status) { case SyncStatus.idle: print('Sync idle'); case SyncStatus.syncing: print('Syncing... (trigger: ${state.lastTrigger})'); case SyncStatus.success: print('Sync completed at ${state.lastSyncTime}'); case SyncStatus.error: print('Sync error: ${state.errorMessage}'); }});
// Check current state synchronouslyfinal currentState = syncManager.currentState;final isSyncing = syncManager.isSyncing;Manual Sync Trigger
Section titled “Manual Sync Trigger”Trigger sync manually:
// Trigger manual syncawait syncManager.sync(trigger: SyncTrigger.manual);
// Enable auto-sync with custom intervalsyncManager.enableAutoSync(interval: Duration(minutes: 10));
// Disable auto-syncsyncManager.disableAutoSync();Error Handling
Section titled “Error Handling”Network Errors
Section titled “Network Errors”When network errors occur:
Transient errors (network timeout, temporary 500 errors):
- Sync automatically retries with exponential backoff
- Local changes remain queued
- UI continues to work offline
Permanent errors (401 unauthorized, 404 not found):
- Sync stops retrying
- Error exposed to UI for user action
- Local changes remain queued
Conflict Errors
Section titled “Conflict Errors”Conflicts are not errors in Locorda - they’re resolved automatically using CRDT merge strategies. See Conflict Resolution.
Storage Errors
Section titled “Storage Errors”If local storage fails (disk full, database corruption):
- Sync engine exposes the error
- App should prompt user to free space or reinstall
- Remote data remains safe
Retry Logic
Section titled “Retry Logic”Exponential Backoff
Section titled “Exponential Backoff”Failed sync attempts retry with increasing delays:
Attempt 1: ImmediateAttempt 2: 1 second delayAttempt 3: 2 seconds delayAttempt 4: 4 seconds delayAttempt 5: 8 seconds delay...Max delay: 60 secondsAfter max delay is reached, sync retries every 60 seconds until successful.
Sync Optimizations
Section titled “Sync Optimizations”Cursor Tracking
Section titled “Cursor Tracking”Locorda uses cursors to track how far it has replayed its local storage into your app’s database.
When hydrateWithCallbacks() is called, Locorda replays stored objects into your app via onUpdate/onDelete. The cursor records the position of the last successfully delivered update. On the next app start, Locorda resumes from that position — so only objects your app hasn’t seen yet are replayed.
getCurrentCursor: () async { // Return the position your app has already processed final prefs = await SharedPreferences.getInstance(); return prefs.getString('sync_cursor_tasks');},onCursorUpdate: (cursor) async { // Persist the position after each successful delivery final prefs = await SharedPreferences.getInstance(); await prefs.setString('sync_cursor_tasks', cursor);}Benefits:
- App startup is fast — only unprocessed updates are replayed
- Survives crashes: replays from last committed cursor, not from scratch
- Essential when your local database already holds the full dataset
Performance Considerations
Section titled “Performance Considerations”Large Datasets
Section titled “Large Datasets”For large datasets:
- Implement pagination in your UI
- Consider lazy loading (fetch on demand, not all at once)
See Advanced Features - Lazy Loading for details.
Frequent Changes
Section titled “Frequent Changes”For rapidly changing data:
- Use debouncing in your UI (avoid save on every keystroke)
- Trust batching - sync engine batches automatically
- Monitor network usage if on mobile data
Debugging Sync Issues
Section titled “Debugging Sync Issues”Enable Logging
Section titled “Enable Logging”final locorda = await initLocorda( remotes: [solidPod], storage: DriftStorageMainHandler(), // setupLogging would be provided by you, the developer onWorkerSpawn: () => setupLogging( level: Level.ALL, // Verbose logging threadName: 'WORKER', ),);Common Issues
Section titled “Common Issues”Changes not syncing:
- Check connection state
- Verify remote credentials
- Check for network errors in logs
Unexpected merge results:
- Review CRDT merge strategies on your models
- Check timestamps (last-write-wins depends on clock sync)
- See Conflict Resolution
Slow sync performance:
- Check dataset size
- Consider lazy loading
- Consider “shard dataset” mode for the backends - this means that all resources of a shard would be stored in the shard file instead of separately as a file per resource.
Next Steps
Section titled “Next Steps”- Conflict Resolution - How conflicts are resolved
- Architecture - Understanding components and layers
- Repository Pattern - Implement sync-aware repositories
Summary
Section titled “Summary”Sync lifecycle in Locorda:
- Local changes: Queued automatically, flow through
onUpdatecallback - Background sync: Automatic, bidirectional, runs in worker thread
- Offline mode: Fully supported, changes queued until reconnection
- Conflict resolution: Automatic via CRDT merge strategies
- Error handling: Exponential backoff retry with error exposure
- Optimizations: Incremental sync (cursors), batching, background processing
The callback pattern ensures all changes (local and remote) flow through a single code path, keeping your UI in sync with the merged state.