offline_first_sync
An intelligent offline-first data management package for Flutter that automatically syncs when online, handles conflicts, and provides visual indicators of sync status.
Features
- 🔄 Automatic Sync: Intelligent syncing when network becomes available
- 📱 Offline-First: All operations work offline with local storage
- ⚡ Conflict Resolution: Multiple strategies for handling sync conflicts
- 🎨 Visual Indicators: Beautiful UI components for sync status
- 🗄️ Local Storage: SQLite-based local persistence
- 🌐 Network Aware: Automatic network connectivity detection
- 🔧 Customizable: Flexible sync intervals and strategies
Installation
Add this to your package's pubspec.yaml file:
dependencies:
offline_first_sync: ^1.0.0
Quick Start
1. Initialize SyncManager
import 'package:offline_first_sync/offline_first_sync.dart';
final syncClient = HttpSyncClient(
baseUrl: 'https://your-api.com/api',
headers: {'Authorization': 'Bearer your-token'},
);
final syncManager = SyncManager(
client: syncClient,
syncInterval: Duration(minutes: 5),
);
2. Add Sync Items
// Create a new item
await syncManager.addSyncItem(
entityType: 'todos',
entityId: 'todo-123',
data: {'title': 'Buy groceries', 'completed': false},
action: SyncAction.create,
);
// Update an existing item
await syncManager.addSyncItem(
entityType: 'todos',
entityId: 'todo-123',
data: {'title': 'Buy groceries', 'completed': true},
action: SyncAction.update,
);
// Delete an item
await syncManager.addSyncItem(
entityType: 'todos',
entityId: 'todo-123',
data: {},
action: SyncAction.delete,
);
3. Monitor Sync Status
StreamBuilder<SyncStatus>(
stream: syncManager.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
return SyncStatusIndicator(
status: status,
onTap: () => showSyncDetails(),
);
},
)
4. Handle Conflicts
syncManager.conflictsStream.listen((conflicts) {
for (final conflict in conflicts) {
// Show conflict resolution UI
showConflictResolutionDialog(conflict);
}
});
// Resolve conflict
await syncManager.resolveConflict(
conflictItem.id,
ConflictResolutionStrategy.clientWins,
);
Core Classes
SyncManager
The main class that orchestrates offline-first synchronization.
final syncManager = SyncManager(
client: syncClient, // Required: Your sync client implementation
storage: localStorage, // Optional: Custom storage implementation
syncInterval: Duration(minutes: 5), // Optional: Auto-sync interval
);
SyncItem
Represents a data change that needs to be synchronized.
final syncItem = SyncItem.create(
entityType: 'users',
entityId: 'user-456',
data: {'name': 'John Doe', 'email': 'john@example.com'},
action: SyncAction.update,
);
SyncStatus
Provides comprehensive information about the current sync state.
class SyncStatus {
final bool isOnline; // Network connectivity status
final bool isSyncing; // Whether sync is currently in progress
final int pendingCount; // Number of items waiting to sync
final int failedCount; // Number of items that failed to sync
final int conflictCount; // Number of items with conflicts
final DateTime? lastSyncTime; // When the last sync completed
final String? errorMessage; // Last error message, if any
}
ConflictResolution
Represents the resolution of a sync conflict between local and server data.
class ConflictResolution {
final String syncItemId; // ID of the sync item that had the conflict
final ConflictResolutionStrategy strategy; // Strategy used to resolve the conflict
final Map<String, dynamic>? resolvedData; // The final resolved data after conflict resolution
final DateTime resolvedAt; // Timestamp when the conflict was resolved
}
Creating a ConflictResolution
final resolution = ConflictResolution(
syncItemId: 'sync-item-123',
strategy: ConflictResolutionStrategy.merge,
resolvedData: {'merged': 'data', 'from': 'both sources'},
resolvedAt: DateTime.now(),
);
UI Components
SyncStatusIndicator
A compact status indicator showing current sync state:
SyncStatusIndicator(
status: syncStatus,
showText: true, // Show status text alongside icon
onTap: () => showSyncDetails(), // Callback when tapped
)
SyncFab
A floating action button for manual sync:
SyncFab(
status: syncStatus,
onPressed: () => syncManager.sync(),
tooltip: 'Sync now',
)
Conflict Resolution Strategies
The package provides several built-in strategies for resolving sync conflicts:
ConflictResolutionStrategy.clientWins
Always uses the local (client-side) data, discarding server changes.
await syncManager.resolveConflict(
conflictItemId,
ConflictResolutionStrategy.clientWins,
);
ConflictResolutionStrategy.serverWins
Always uses the server data, discarding local changes.
await syncManager.resolveConflict(
conflictItemId,
ConflictResolutionStrategy.serverWins,
);
ConflictResolutionStrategy.lastWriteWins
Uses the data with the most recent timestamp.
await syncManager.resolveConflict(
conflictItemId,
ConflictResolutionStrategy.lastWriteWins,
);
ConflictResolutionStrategy.merge
Automatically merges both versions using a built-in merge algorithm.
await syncManager.resolveConflict(
conflictItemId,
ConflictResolutionStrategy.merge,
);
ConflictResolutionStrategy.manual
Allows custom resolution with user-provided data.
await syncManager.resolveConflict(
conflictItemId,
ConflictResolutionStrategy.manual,
customData: {'manually': 'resolved', 'data': 'here'},
);
Custom Sync Client
Implement your own sync client by extending the SyncClient abstract class:
class CustomSyncClient implements SyncClient {
@override
Future<SyncResponse> syncItem(SyncItem item) async {
// Your custom sync logic here
try {
// Perform the sync operation
final result = await performCustomSync(item);
return SyncResponse(
success: true,
data: result,
);
} on ConflictException catch (e) {
return SyncResponse(
success: false,
hasConflict: true,
serverData: e.serverData,
serverVersion: e.version,
);
} catch (e) {
return SyncResponse(
success: false,
error: e.toString(),
);
}
}
@override
Future<Map<String, dynamic>?> fetchLatestData(
String entityType,
String entityId
) async {
// Fetch latest data from your backend
return await getDataFromBackend(entityType, entityId);
}
}
Advanced Usage
Custom Storage
final customStorage = LocalStorage();
final syncManager = SyncManager(
client: syncClient,
storage: customStorage,
);
Monitoring Sync Operations
// Listen to sync status changes
syncManager.statusStream.listen((status) {
print('Sync status: ${status.isOnline ? 'Online' : 'Offline'}');
print('Pending items: ${status.pendingCount}');
print('Failed items: ${status.failedCount}');
print('Conflicts: ${status.conflictCount}');
});
// Listen to conflict events
syncManager.conflictsStream.listen((conflicts) {
print('Active conflicts: ${conflicts.length}');
for (final conflict in conflicts) {
print('Conflict in ${conflict.entityType}:${conflict.entityId}');
}
});
Retry Failed Items
await syncManager.retryFailed();
Clear Sync Queue
await syncManager.clearSyncQueue();
Manual Sync
await syncManager.sync();
Error Handling
syncManager.statusStream.listen((status) {
if (status.errorMessage != null) {
// Handle sync errors
showErrorDialog(status.errorMessage!);
}
});
Sync States
Each sync item can be in one of several states:
SyncState.pending: Waiting to be synchronizedSyncState.syncing: Currently being synchronizedSyncState.synced: Successfully synchronized (item removed from queue)SyncState.failed: Synchronization failed (will retry)SyncState.conflict: Conflict detected, requires resolution
Sync Actions
Three types of sync actions are supported:
SyncAction.create: Create a new entity on the serverSyncAction.update: Update an existing entity on the serverSyncAction.delete: Delete an entity from the server
Best Practices
1. Entity Design
Use consistent entity types and IDs across your application:
// Good: Consistent naming
await syncManager.addSyncItem(
entityType: 'todos',
entityId: todo.id,
data: todo.toMap(),
action: SyncAction.update,
);
2. Data Serialization
Ensure your data is JSON-serializable:
// Good: Simple, serializable data
final data = {
'title': 'Task title',
'completed': false,
'createdAt': DateTime.now().toIso8601String(),
};
3. Conflict Prevention
Design your data model to minimize conflicts:
// Include version or timestamp fields
final data = {
'title': 'Updated title',
'lastModified': DateTime.now().toIso8601String(),
'version': currentVersion + 1,
};
4. Error Monitoring
Always monitor sync status for errors:
StreamBuilder<SyncStatus>(
stream: syncManager.statusStream,
builder: (context, snapshot) {
final status = snapshot.data;
if (status?.hasIssues == true) {
return ErrorIndicator(status: status);
}
return SyncStatusIndicator(status: status);
},
)
API Reference
SyncManager Methods
addSyncItem(): Add an item to the sync queuesync(): Manually trigger synchronizationresolveConflict(): Resolve a conflict with specified strategyretryFailed(): Retry all failed sync itemsclearSyncQueue(): Clear all pending sync itemsdispose(): Clean up resources
SyncManager Properties
statusStream: Stream of sync status updatesconflictsStream: Stream of conflict notificationscurrentStatus: Current sync status
Example
See the /example folder for a complete todo app implementation using offline_first_sync.
Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
License
This project is licensed under the MIT License - see the LICENSE file for details.