flutter_sync_manager_pro
Offline-first data synchronization engine for Flutter applications.
Build apps that work seamlessly offline with automatic background synchronization when online. Perfect for mobile apps with unreliable connectivity, multi-device sync, and real-time collaboration features.
Features
- ✅ Offline-first: Write and read data locally, sync automatically when online
- ✅ Delta sync: Only sync changes, not entire datasets
- ✅ Conflict resolution: Smart merge strategies for concurrent edits
- ✅ Background sync: Non-blocking synchronization with configurable intervals
- ✅ Priority-based: Critical data syncs first
- ✅ Queue management: Reliable sync queue with automatic retry
- ✅ Generic adapters: Works with any backend (REST API, Firebase, GraphQL, etc.)
- ✅ Multi-device support: Sync data across devices seamlessly
- ✅ Production-ready: Built for real-world apps
Use Cases
- Travel Apps: Routes, itineraries, and bookings work offline
- E-commerce: Browse products and cart offline, sync on reconnect
- Note-Taking: Write notes offline, sync across devices
- Task Management: Manage todos without internet
- Collaboration Tools: Handle concurrent edits gracefully
- Forms & Surveys: Collect data offline, submit later
Installation
dependencies:
flutter_sync_manager_pro: ^1.0.0
Then run:
flutter pub get
Quick Start
1. Initialize the Sync Engine
import 'package:flutter_sync_manager_pro/flutter_sync_manager_pro.dart';
final syncEngine = SyncEngine(
adapter: RestApiAdapter(
baseUrl: 'https://api.example.com',
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
),
config: SyncConfig(
autoSync: true,
syncInterval: Duration(minutes: 5),
enableDeltaSync: true,
),
);
await syncEngine.initialize();
2. Save Data (Works Offline!)
// Save an item - works even without internet
await syncEngine.save('todos', {
'id': '123',
'title': 'Buy groceries',
'completed': false,
'createdAt': DateTime.now().toIso8601String(),
});
// Will automatically sync to server when online
3. Read Data (Always Fast!)
// Get a specific item
final todo = await syncEngine.get('todos', '123');
// Get all items in a collection
final allTodos = await syncEngine.getAll('todos');
4. Delete Data
await syncEngine.delete('todos', '123');
// Deletion will sync when online
5. Monitor Sync Status
syncEngine.statsStream.listen((stats) {
print('Pending: ${stats.pendingCount}');
print('Synced: ${stats.syncedCount}');
print('Errors: ${stats.errorCount}');
print('Last sync: ${stats.lastSyncTime}');
});
Configuration
SyncConfig
final config = SyncConfig(
// Enable automatic syncing
autoSync: true,
// Sync every 5 minutes
syncInterval: Duration(minutes: 5),
// Maximum retry attempts for failed syncs
maxRetries: 3,
// Delay between retries (exponential backoff)
retryDelay: Duration(seconds: 5),
// Enable delta sync (only sync changes)
enableDeltaSync: true,
// Batch size for sync operations
batchSize: 50,
// Timeout for sync requests
timeout: Duration(seconds: 30),
// Enable background sync
enableBackgroundSync: true,
// Enable logging for debugging
enableLogging: false,
);
Conflict Resolution
Choose how to handle conflicts when the same data is modified on multiple devices:
final syncEngine = SyncEngine(
adapter: adapter,
conflictResolver: ConflictResolver(
strategy: ConflictStrategy.serverWins,
),
);
Available Strategies
| Strategy | Description |
|---|---|
serverWins |
Server version always wins (default) |
clientWins |
Client version always wins |
merge |
Field-level merge of both versions |
newestWins |
Use version with latest timestamp |
keepBoth |
Keep both versions for manual resolution |
custom |
Provide your own resolver function |
Custom Conflict Resolver
final syncEngine = SyncEngine(
adapter: adapter,
conflictResolver: ConflictResolver(
strategy: ConflictStrategy.custom,
customResolver: (conflict) async {
// Your custom logic here
final clientData = conflict.clientRecord.data;
final serverData = conflict.serverData;
// Example: Merge specific fields
final resolved = {
...serverData,
'localNotes': clientData['localNotes'],
};
return ConflictResolution(
resolvedData: resolved,
shouldSync: true,
message: 'Merged with custom logic',
);
},
),
);
Priority-Based Syncing
Mark important data to sync immediately:
// Normal priority (syncs in queue)
await syncEngine.save('notes', data);
// High priority (triggers immediate sync)
await syncEngine.save(
'payments',
data,
priority: SyncPriority.high,
);
// Critical priority (syncs ASAP)
await syncEngine.save(
'emergency_alerts',
data,
priority: SyncPriority.critical,
);
Custom Backend Adapters
REST API Adapter
final adapter = RestApiAdapter(
baseUrl: 'https://api.example.com',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
'X-API-Key': 'your-api-key',
},
timeout: Duration(seconds: 30),
);
Your backend should implement a /sync endpoint:
POST /sync
Request:
{
"changes": [
{
"id": "record-1",
"collection": "todos",
"itemId": "123",
"changeType": "create",
"data": {"id": "123", "title": "Buy milk"},
"createdAt": "2024-01-01T00:00:00.000Z"
}
],
"lastSyncTime": "2024-01-01T00:00:00.000Z"
}
Response:
{
"syncedItems": {"123": "v1"},
"conflicts": {},
"errors": {},
"serverTime": "2024-01-01T00:05:00.000Z",
"serverChanges": []
}
Firebase Adapter
// Firebase Realtime Database
final adapter = FirebaseAdapter.realtimeDatabase(
projectId: 'my-project',
databaseUrl: 'https://my-project.firebaseio.com',
authToken: 'YOUR_ID_TOKEN',
);
// Or Cloud Firestore
final adapter = FirebaseAdapter.firestore(
projectId: 'my-project',
authToken: 'YOUR_ID_TOKEN',
);
Note: For production Firebase apps, we recommend using the official Firebase SDKs (firebase_core, cloud_firestore) and creating a custom adapter that wraps them.
Creating a Custom Adapter
class MyCustomAdapter extends SyncAdapter {
final GraphQLClient client;
MyCustomAdapter(this.client);
@override
Future<SyncResponse> sync(SyncRequest request) async {
// Your sync implementation
final result = await client.mutate(
MutationOptions(
document: gql('''
mutation SyncChanges(\$changes: [ChangeInput!]!) {
sync(changes: \$changes) {
syncedItems
conflicts
errors
}
}
'''),
variables: {'changes': request.changes},
),
);
return SyncResponse.fromJson(result.data!['sync']);
}
@override
Future<bool> checkConnection() async {
try {
await client.query(QueryOptions(document: gql('{ ping }')));
return true;
} catch (e) {
return false;
}
}
}
Advanced Usage
Manual Sync Trigger
// Manually trigger sync
await syncEngine.sync();
Stop/Start Auto-Sync
// Stop automatic syncing
syncEngine.stopAutoSync();
// Start automatic syncing
syncEngine.startAutoSync();
Get Sync Statistics
final stats = await syncEngine.getStats();
print('Pending items: ${stats.pendingCount}');
print('Synced items: ${stats.syncedCount}');
print('Errors: ${stats.errorCount}');
print('Conflicts: ${stats.conflictCount}');
print('Last sync: ${stats.lastSyncTime}');
Clean Up Synced Records
// Remove successfully synced records from queue
await syncEngine.clearSyncedRecords();
Complete Example
import 'package:flutter/material.dart';
import 'package:flutter_sync_manager_pro/flutter_sync_manager_pro.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize sync engine
final syncEngine = SyncEngine(
adapter: RestApiAdapter(
baseUrl: 'https://api.example.com',
headers: {'Authorization': 'Bearer TOKEN'},
),
config: SyncConfig(
autoSync: true,
syncInterval: Duration(minutes: 5),
),
);
await syncEngine.initialize();
runApp(MyApp(syncEngine: syncEngine));
}
class MyApp extends StatelessWidget {
final SyncEngine syncEngine;
const MyApp({required this.syncEngine, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: TodoScreen(syncEngine: syncEngine),
);
}
}
class TodoScreen extends StatefulWidget {
final SyncEngine syncEngine;
const TodoScreen({required this.syncEngine, Key? key}) : super(key: key);
@override
State<TodoScreen> createState() => _TodoScreenState();
}
class _TodoScreenState extends State<TodoScreen> {
List<Map<String, dynamic>> todos = [];
@override
void initState() {
super.initState();
_loadTodos();
// Listen to sync status
widget.syncEngine.statsStream.listen((stats) {
print('Sync status: ${stats.pendingCount} pending');
});
}
Future<void> _loadTodos() async {
final data = await widget.syncEngine.getAll('todos');
setState(() => todos = data);
}
Future<void> _addTodo(String title) async {
final todo = {
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'title': title,
'completed': false,
'createdAt': DateTime.now().toIso8601String(),
};
await widget.syncEngine.save('todos', todo);
await _loadTodos();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Offline Todos')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo['title']),
trailing: Icon(
todo['completed'] ? Icons.check_circle : Icons.circle_outlined,
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addTodo('New Todo'),
child: Icon(Icons.add),
),
);
}
}
Testing
For testing, you can inject a mock adapter:
class MockAdapter extends SyncAdapter {
@override
Future<SyncResponse> sync(SyncRequest request) async {
// Mock response
return SyncResponse(
syncedItems: {for (var r in request.changes) r.itemId: 'v1'},
conflicts: {},
errors: {},
serverTime: DateTime.now(),
);
}
@override
Future<bool> checkConnection() async => true;
}
// In tests
final syncEngine = SyncEngine(adapter: MockAdapter());
await syncEngine.initialize();
Best Practices
- Initialize early: Call
syncEngine.initialize()inmain()beforerunApp() - Use priorities wisely: Only mark truly critical data as high/critical priority
- Handle conflicts: Choose an appropriate conflict resolution strategy for your use case
- Monitor stats: Display sync status in your UI so users know when data is syncing
- Clean up: Call
syncEngine.dispose()when done (e.g., in app shutdown) - Test offline: Always test your app in airplane mode
- Delta sync: Enable delta sync for apps with large datasets
Limitations
- Local storage uses SharedPreferences by default (simple but limited). For production apps with large datasets, consider implementing a custom adapter with a proper local database (sqflite, hive, isar).
- The Firebase adapter is REST-based. For production Firebase apps, use the official SDKs and create a custom adapter.
- Background sync on iOS requires additional configuration (background fetch capability).
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Support
- Issues: GitHub Issues
- Email: info@flixelo.com
Changelog
See CHANGELOG.md for version history.
Libraries
- flutter_sync_manager_pro
- Offline-first data synchronization engine for Flutter.