offline_first_sync 1.1.5
offline_first_sync: ^1.1.5 copied to clipboard
Intelligent offline-first data management with automatic sync, conflict resolution, and visual status indicators
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.1.5
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 data synchronization.
Constructor:
SyncManager({
required SyncClient client,
LocalStorage? storage,
Duration syncInterval = const Duration(minutes: 5),
})
Key Methods:
addSyncItem()- Adds an operation to the sync queuesync()- Manually triggers synchronizationresolveConflict()- Resolves sync conflictsretryFailed()- Retries failed sync operationsclearSyncQueue()- Clears all pending sync items
Streams:
statusStream- Real-time sync status updatesconflictsStream- Stream of conflict items requiring resolution
SyncItem #
Represents a single synchronization operation.
Properties:
id- Unique identifier for the sync operationentityType- Type of entity being synced (e.g., 'todos', 'users')entityId- Unique identifier of the entitydata- The data to be synchronizedaction- Type of operation (create, update, delete)state- Current sync state (pending, syncing, synced, failed, conflict)createdAt- When the sync item was createdlastSyncAttempt- Last time sync was attemptedretryCount- Number of retry attemptsconflictVersion- Server version causing conflictserverData- Server data in case of conflict
SyncStatus #
Provides comprehensive information about the current synchronization state.
Properties:
isOnline- Whether the device has internet connectivityisSyncing- Whether a sync operation is currently in progresspendingCount- Number of items waiting to be syncedfailedCount- Number of items that failed to syncconflictCount- Number of items with sync conflictslastSyncTime- Timestamp of the last successful syncerrorMessage- Last error message, if any
Computed Properties:
hasIssues- Returns true if there are failed or conflicted itemshasPendingItems- Returns true if there are pending itemsisFullySynced- Returns true if everything is synchronized
ConflictResolution #
Represents the resolution of a sync conflict.
Constructor:
ConflictResolution({
required String syncItemId,
required ConflictResolutionStrategy strategy,
Map<String, dynamic>? resolvedData,
required DateTime resolvedAt,
})
Properties:
syncItemId- ID of the sync item that had the conflictstrategy- The resolution strategy that was appliedresolvedData- The final data after conflict resolution (optional, used with manual strategy)resolvedAt- Timestamp when the conflict was resolved
Usage Example:
final resolution = ConflictResolution(
syncItemId: 'sync-item-123',
strategy: ConflictResolutionStrategy.manual,
resolvedData: {'title': 'Merged title', 'completed': true},
resolvedAt: DateTime.now(),
);
UI Components #
SyncStatusIndicator #
A compact status indicator showing current sync state:
SyncStatusIndicator(
status: syncStatus,
showText: true,
onTap: () => showSyncDetails(),
)
Properties:
status- Current SyncStatusonTap- Callback when indicator is tappedshowText- Whether to show status text alongside icon
SyncFab #
A floating action button for manual sync:
SyncFab(
status: syncStatus,
onPressed: () => syncManager.sync(),
tooltip: 'Sync now',
)
Properties:
status- Current SyncStatusonPressed- Callback when FAB is pressedtooltip- Optional tooltip text
Conflict Resolution Strategies #
The package provides several built-in strategies for handling sync conflicts:
ConflictResolutionStrategy.clientWins #
Always uses the local/client data, overwriting server changes.
await syncManager.resolveConflict(
conflictId,
ConflictResolutionStrategy.clientWins,
);
ConflictResolutionStrategy.serverWins #
Always uses the server data, discarding local changes.
await syncManager.resolveConflict(
conflictId,
ConflictResolutionStrategy.serverWins,
);
ConflictResolutionStrategy.lastWriteWins #
Uses the data with the most recent timestamp.
await syncManager.resolveConflict(
conflictId,
ConflictResolutionStrategy.lastWriteWins,
);
ConflictResolutionStrategy.merge #
Automatically attempts to merge both versions of the data.
await syncManager.resolveConflict(
conflictId,
ConflictResolutionStrategy.merge,
);
ConflictResolutionStrategy.manual #
Allows custom data to be provided for conflict resolution.
await syncManager.resolveConflict(
conflictId,
ConflictResolutionStrategy.manual,
customData: {'title': 'User edited title', 'completed': true},
);
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 yourApiCall(item);
return SyncResponse(
success: true,
data: result,
);
} catch (e) {
return SyncResponse(
success: false,
error: e.toString(),
);
}
}
@override
Future<Map<String, dynamic>?> fetchLatestData(
String entityType,
String entityId
) async {
// Fetch the latest data from your backend
try {
final response = await yourFetchCall(entityType, entityId);
return response.data;
} catch (e) {
return null;
}
}
}
Advanced Usage #
Custom Storage #
final customStorage = LocalStorage();
final syncManager = SyncManager(
client: syncClient,
storage: customStorage,
);
Monitoring Sync Events #
// 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}');
});
// Listen to conflicts
syncManager.conflictsStream.listen((conflicts) {
print('Conflicts to resolve: ${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 #
The package provides comprehensive error handling:
// Check for sync errors
if (syncStatus.errorMessage != null) {
print('Sync error: ${syncStatus.errorMessage}');
}
// Handle failed items
if (syncStatus.failedCount > 0) {
// Show retry option to user
await syncManager.retryFailed();
}
// Handle conflicts
if (syncStatus.conflictCount > 0) {
// Show conflict resolution UI
final conflicts = await syncManager.conflictsStream.first;
// Present resolution options to user
}
Best Practices #
- Initialize Early: Set up SyncManager in your app's initialization phase
- Handle Conflicts Gracefully: Always provide UI for conflict resolution
- Monitor Status: Use status indicators to keep users informed
- Batch Operations: Group related operations when possible
- Handle Errors: Implement proper error handling and retry mechanisms
- Test Offline: Thoroughly test your app's offline capabilities
API Reference #
Enums #
SyncAction
create- Create a new entityupdate- Update an existing entitydelete- Delete an entity
SyncState
pending- Waiting to be syncedsyncing- Currently being syncedsynced- Successfully synchronizedfailed- Sync failedconflict- Conflict detected during sync
ConflictResolutionStrategy
manual- User decides resolutionclientWins- Always use client dataserverWins- Always use server datalastWriteWins- Use data with latest timestampmerge- Attempt to merge both versions
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.