SynQ Manager
A powerful synchronization manager for Flutter apps with secure local storage, real-time state management, background cloud sync capabilities, and Socket.io-style event handling.
β¨ Features
π― Simplified Socket.io Style Events: Clean, intuitive event handling with only essential callbacks
-
onEvent
- Universal event listener for all events -
onInit
- Initialization with all existing data -
onCreate
- New item creation events -
onUpdate
- Item modification events -
onDelete
- Item deletion events -
onError
- Error handling -
Real-time Synchronization: Automatic cloud sync with configurable intervals
-
π± Background Sync: Uses WorkManager for background synchronization when app is closed
-
π Secure Storage: Encrypted local storage with Hive Plus Secure
-
βοΈ Conflict Resolution: Intelligent conflict handling with multiple resolution strategies
-
π Connectivity Aware: Automatic sync when network becomes available
-
π― Type-safe API: Full TypeScript-like generics support for type safety
-
β‘ High Performance: Optimized for mobile with single-instance listeners
-
π§ Customizable: Flexible configuration for different use cases
-
π Event-driven: Real-time event streams for UI updates
-
π Local-first: Works offline, syncs when online
-
β Cascade Notation: Efficient single-instance listener pattern
π Platform Support
Mobile Only: This package is designed for mobile platforms (Android & iOS) due to WorkManager dependency requirements.
- β Android
- β iOS
- β Web (WorkManager not supported)
- β Desktop (WorkManager not supported)
π¦ Installation
Add this to your pubspec.yaml
:
dependencies:
synq_manager: latest_version
Run:
flutter pub get
π οΈ WorkManager Setup
SynQ Manager uses WorkManager for background synchronization. Follow these platform-specific setup instructions:
Android Setup
- Minimum SDK Version: Add to
android/app/build.gradle
:
android {
compileSdkVersion 34
defaultConfig {
minSdkVersion 23 // WorkManager requires API 23+
targetSdkVersion 34
}
}
- Permissions: Add to
android/app/src/main/AndroidManifest.xml
:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- For background sync -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
- WorkManager Service: Add to
android/app/src/main/AndroidManifest.xml
inside<application>
:
<service
android:name="be.tramckrijte.workmanager.BackgroundService"
android:exported="false" />
<receiver
android:name="be.tramckrijte.workmanager.BackgroundService$AlarmReceiver"
android:exported="false" />
iOS Setup
- Minimum iOS Version: Update
ios/Podfile
:
platform :ios, '12.0' # WorkManager requires iOS 12.0+
- Background Modes: Add to
ios/Runner/Info.plist
:
<key>UIBackgroundModes</key>
<array>
<string>background-processing</string>
<string>background-fetch</string>
</array>
- Background App Refresh: Add to
ios/Runner/Info.plist
:
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>be.tramckrijte.workmanager.BackgroundService</string>
</array>
π― Quick Start
1. Basic Setup
import 'package:synq_manager/synq_manager.dart';
// Define your data model
class UserProfile {
final String id;
final String name;
final String email;
UserProfile({required this.id, required this.name, required this.email});
// Add serialization methods
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
// Initialize SynqManager
late SynqManager<UserProfile> userManager;
Future<void> initializeSynq() async {
userManager = await SynqManager.getInstance<UserProfile>(
instanceName: 'user_profiles',
config: SyncConfig(
syncInterval: Duration(minutes: 5),
enableBackgroundSync: true,
encryptionKey: 'your-encryption-key', // Optional
),
cloudSyncFunction: _syncToCloud,
cloudFetchFunction: _fetchFromCloud,
fromJson: UserProfile.fromJson, // Function to deserialize UserProfile from JSON
toJson: (profile) => profile.toJson(), // Function to serialize UserProfile to JSON
);
}
Important: The fromJson
and toJson
parameters are required when working with complex custom objects that need proper JSON serialization/deserialization. For simple types like String
, int
, Map<String, dynamic>
, these parameters can be omitted.
2. Implement Cloud Functions
// Sync local changes to cloud
Future<SyncResult<UserProfile>> _syncToCloud(
Map<String, SyncData<UserProfile>> localChanges,
Map<String, String> headers,
) async {
try {
// Your API call logic here
final response = await http.post(
Uri.parse('https://your-api.com/sync'),
headers: {'Content-Type': 'application/json', ...headers},
body: jsonEncode({
'changes': localChanges.map((key, data) => MapEntry(key, {
'value': data.value.toJson(),
'version': data.version,
'timestamp': data.timestamp,
'deleted': data.deleted,
})),
}),
);
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final remoteData = <String, SyncData<UserProfile>>{};
// Parse remote data
for (final entry in responseData['data'].entries) {
remoteData[entry.key] = SyncData<UserProfile>(
value: UserProfile.fromJson(entry.value['value']),
version: entry.value['version'],
timestamp: entry.value['timestamp'],
deleted: entry.value['deleted'] ?? false,
);
}
return SyncResult<UserProfile>(
success: true,
remoteData: remoteData,
conflicts: [], // Handle conflicts if any
);
} else {
throw Exception('Sync failed: ${response.statusCode}');
}
} catch (error) {
return SyncResult<UserProfile>(
success: false,
error: error,
);
}
}
// Fetch updates from cloud
Future<Map<String, SyncData<UserProfile>>> _fetchFromCloud(
int lastSyncTimestamp,
Map<String, String> headers,
) async {
try {
final response = await http.get(
Uri.parse('https://your-api.com/updates?since=$lastSyncTimestamp'),
headers: headers,
);
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
final remoteData = <String, SyncData<UserProfile>>{};
for (final entry in responseData['data'].entries) {
remoteData[entry.key] = SyncData<UserProfile>(
value: UserProfile.fromJson(entry.value['value']),
version: entry.value['version'],
timestamp: entry.value['timestamp'],
deleted: entry.value['deleted'] ?? false,
);
}
return remoteData;
} else {
throw Exception('Fetch failed: ${response.statusCode}');
}
} catch (error) {
return {};
}
}
3. Use the Manager
class UserProfileScreen extends StatefulWidget {
@override
_UserProfileScreenState createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State<UserProfileScreen> {
StreamSubscription<SynqEvent<UserProfile>>? _subscription;
List<UserProfile> _profiles = [];
@override
void initState() {
super.initState();
_setupListener();
_loadProfiles();
}
void _setupListener() {
_subscription = userManager.onData.listen((event) {
setState(() {
// Update UI based on events
switch (event.type) {
case SynqEventType.create:
case SynqEventType.update:
_loadProfiles(); // Refresh list
break;
case SynqEventType.delete:
_profiles.removeWhere((p) => p.id == event.key);
break;
}
});
});
// Listen to sync events
userManager.onDone.listen((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sync completed')),
);
});
userManager.onError.listen((event) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Sync error: ${event.error}')),
);
});
}
Future<void> _loadProfiles() async {
final profiles = await userManager.getAll();
setState(() {
_profiles = profiles.values.toList();
});
}
Future<void> _addProfile() async {
final profile = UserProfile(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: 'New User',
email: 'user@example.com',
);
await userManager.put(profile.id, profile);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User Profiles'),
actions: [
IconButton(
icon: Icon(Icons.sync),
onPressed: () => userManager.sync(),
),
],
),
body: Column(
children: [
// Sync status
Container(
padding: EdgeInsets.all(16),
child: Row(
children: [
Icon(userManager.connectivityStatus == ConnectivityStatus.online
? Icons.cloud_done : Icons.cloud_off),
SizedBox(width: 8),
Text(userManager.isSyncing ? 'Syncing...' : 'Ready'),
Spacer(),
Text('Pending: ${userManager.pendingChangesCount}'),
],
),
),
// Profile list
Expanded(
child: ListView.builder(
itemCount: _profiles.length,
itemBuilder: (context, index) {
final profile = _profiles[index];
return ListTile(
title: Text(profile.name),
subtitle: Text(profile.email),
onTap: () => _editProfile(profile),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _addProfile,
child: Icon(Icons.add),
),
);
),
),
],
);
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
3B. Socket.io Style Usage (New! π)
For a more intuitive and less boilerplate approach, use the new Socket.io-style API:
class UserProfileScreen extends StatefulWidget {
@override
_UserProfileScreenState createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State<UserProfileScreen> {
List<UserProfile> _profiles = [];
bool _syncing = false;
SynqListeners<UserProfile>? _listeners;
@override
void initState() {
super.initState();
_setupSocketStyleListeners();
}
void _setupSocketStyleListeners() {
// Simplified Socket.io style - single instance with cascade notation
_listeners = userManager.on()
..onInit((allProfiles) {
// Called when manager is ready with ALL data
print('π₯ Loaded ${allProfiles.length} profiles');
setState(() {
_profiles = allProfiles.values.toList();
});
})
..onCreate((key, profile) {
// Called when NEW profile is created - only new data
print('β¨ New profile created: ${profile.name}');
setState(() {
_profiles.add(profile);
});
})
..onUpdate((key, profile) {
// Called when profile is updated - only updated data
print('π Profile updated: ${profile.name}');
setState(() {
final index = _profiles.indexWhere((p) => p.id == key);
if (index != -1) _profiles[index] = profile;
});
})
..onDelete((key) {
// Called when profile is deleted - only key
print('ποΈ Profile deleted: $key');
setState(() {
_profiles.removeWhere((p) => p.id == key);
});
})
..onError((error) {
setState(() => _syncing = false);
_showError('Sync failed: $error');
})
..onEvent((event) {
// Listen to all events - general callback
print('π Event: ${event.type}');
switch (event.type) {
case SynqEventType.syncStart:
setState(() => _syncing = true);
break;
case SynqEventType.syncComplete:
setState(() => _syncing = false);
_showMessage('Sync completed! β
');
break;
default:
break;
}
});
}
void _showMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
void _showError(String error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(error),
backgroundColor: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User Profiles'),
actions: [
if (_syncing)
Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
else
IconButton(
icon: Icon(Icons.sync),
onPressed: () => userManager.sync(),
),
],
),
body: ListView.builder(
itemCount: _profiles.length,
itemBuilder: (context, index) {
final profile = _profiles[index];
return ListTile(
title: Text(profile.name),
subtitle: Text(profile.email),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => userManager.delete(profile.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _addProfile,
child: Icon(Icons.add),
),
);
}
Future<void> _addProfile() async {
final profile = UserProfile(
id: DateTime.now().millisecondsSinceEpoch.toString(),
name: 'New User ${_profiles.length + 1}',
email: 'user${_profiles.length + 1}@example.com',
);
// This will automatically trigger onCreate() callback
await userManager.put(profile.id, profile);
}
@override
void dispose() {
_listeners?.dispose(); // Clean up listeners
super.dispose();
}
}
Socket.io Style API Reference
Method | When Called | Data Provided | Use Case |
---|---|---|---|
onEvent(callback) |
All events | Event object | Listen to all events in one place |
onInit(callback) |
Manager ready | All existing data | Initialize UI with all data |
onCreate(callback) |
New item added | Only new item | Add item to UI |
onUpdate(callback) |
Item modified | Only updated item | Update item in UI |
onDelete(callback) |
Item removed | Only key | Remove item from UI |
onError(callback) |
Error occurs | Error object | Show error message |
Key Benefits of Simplified API:
- π Single Instance: Uses cascade notation with one listener instance instead of multiple
- β‘ Better Performance: Reduced memory footprint and improved efficiency
- π― Essential Callbacks Only: Removed 10+ redundant callbacks, kept only the necessary 6
- π Universal Event Handler:
onEvent()
captures all events for advanced use cases - οΏ½ Clean Code: Intuitive API that's easy to understand and maintain
Pattern Comparison:
// β Old: Creates multiple instances
manager.onInit(...).onCreate(...).onUpdate(...)
// β
New: Single instance with cascade
final listeners = manager.on();
listeners..onInit(...)..onCreate(...)..onUpdate(...);
π§ Advanced Configuration
final config = SyncConfig(
// Sync frequency
syncInterval: Duration(minutes: 5),
// Batch processing
batchSize: 50,
maxRetries: 3,
retryDelay: Duration(seconds: 2),
// Network settings
requestTimeout: Duration(seconds: 30),
connectTimeout: Duration(seconds: 10),
// Encryption (AES-256)
encryptionKey: 'your-32-character-encryption-key',
// Conflict resolution strategy
conflictResolution: ConflictResolution.lastWriteWins,
βοΈ Configuration Options
SyncConfig
final config = SyncConfig(
// Sync interval (default: 5 minutes)
syncInterval: Duration(minutes: 5),
// Retry attempts for failed syncs (default: 3)
retryAttempts: 3,
// Delay between retries (default: 2 seconds)
retryDelay: Duration(seconds: 2),
// Batch size for bulk operations (default: 50)
batchSize: 50,
// Encryption key for local storage (optional)
encryptionKey: 'your-secret-key',
// Sync priority (default: normal)
priority: SyncPriority.high,
// Enable background sync (default: true)
enableBackgroundSync: true,
// Enable automatic retry (default: true)
enableAutoRetry: true,
// Enable conflict resolution (default: true)
enableConflictResolution: true,
// Maximum storage size in MiB (default: 100)
maxStorageSize: 100,
// Enable compression (default: true)
compressionEnabled: true,
// Custom headers for API calls
customHeaders: {
'Authorization': 'Bearer $token',
'X-API-Version': '1.0',
},
);
Predefined Configurations
// High priority - frequent syncs
final config = SyncConfig.highPriority(
encryptionKey: 'key',
customHeaders: {'Authorization': 'Bearer $token'},
);
// Low priority - less frequent syncs
final config = SyncConfig.lowPriority();
// Mobile optimized - smaller batches, longer intervals
final config = SyncConfig.mobile();
π Conflict Resolution
Handle data conflicts when the same data is modified both locally and remotely:
// Listen for conflicts
userManager.onConflict.listen((event) async {
final conflict = userManager.activeConflicts[event.key];
if (conflict != null) {
// Show conflict resolution UI
await _showConflictDialog(conflict);
}
});
Future<void> _showConflictDialog(DataConflict<UserProfile> conflict) async {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Data Conflict'),
content: Column(
children: [
Text('Local: ${conflict.localData.value.name}'),
Text('Remote: ${conflict.remoteData.value.name}'),
],
),
actions: [
TextButton(
onPressed: () {
// Use local version
userManager.resolveConflict(
conflict.key,
ConflictResolutionStrategy.useLocal,
);
Navigator.pop(context);
},
child: Text('Keep Local'),
),
TextButton(
onPressed: () {
// Use remote version
userManager.resolveConflict(
conflict.key,
ConflictResolutionStrategy.useRemote,
);
Navigator.pop(context);
},
child: Text('Use Remote'),
),
TextButton(
onPressed: () {
// Use custom merge logic
userManager.resolveConflict(
conflict.key,
ConflictResolutionStrategy.merge,
customResolver: (local, remote) {
// Custom merge logic
return local.copyWith(
value: UserProfile(
id: local.value.id,
name: remote.value.name, // Use remote name
email: local.value.email, // Keep local email
),
);
},
);
Navigator.pop(context);
},
child: Text('Merge'),
),
],
),
);
}
π Monitoring & Statistics
// Get sync statistics
final syncStats = userManager.syncStats;
print('Last sync: ${syncStats.timeSinceLastSync}');
print('Pending changes: ${syncStats.pendingChangesCount}');
print('Active conflicts: ${syncStats.activeConflictsCount}');
// Get storage statistics
final storageStats = await userManager.storageStats;
print('Total items: ${storageStats.totalItems}');
print('Storage size: ${storageStats.sizeInBytes} bytes');
// Monitor connectivity
userManager.onConnected.listen((_) {
print('Connected to internet');
});
userManager.onDisconnected.listen((_) {
print('Lost internet connection');
});
π§ͺ Testing
For testing, you can mock the cloud functions:
// Mock sync function for testing
Future<SyncResult<TestModel>> mockSyncFunction(
Map<String, SyncData<TestModel>> localChanges,
Map<String, String> headers,
) async {
// Simulate network delay
await Future.delayed(Duration(milliseconds: 100));
return SyncResult<TestModel>(
success: true,
remoteData: {},
);
}
// Mock fetch function for testing
Future<Map<String, SyncData<TestModel>>> mockFetchFunction(
int lastSyncTimestamp,
Map<String, String> headers,
) async {
await Future.delayed(Duration(milliseconds: 100));
return {};
}
// Use in tests
final testManager = await SynqManager.getInstance<TestModel>(
instanceName: 'test',
cloudSyncFunction: mockSyncFunction,
cloudFetchFunction: mockFetchFunction,
);
π Advanced Usage
Multiple Managers
You can create multiple managers for different data types:
final userManager = await SynqManager.getInstance<User>(
instanceName: 'users',
cloudSyncFunction: syncUsers,
cloudFetchFunction: fetchUsers,
);
final postManager = await SynqManager.getInstance<Post>(
instanceName: 'posts',
cloudSyncFunction: syncPosts,
cloudFetchFunction: fetchPosts,
);
Custom Event Handling
// Listen to specific events
userManager.onEvent(SynqEventType.syncStart).listen((_) {
// Show loading indicator
});
userManager.onEvent(SynqEventType.syncComplete).listen((_) {
// Hide loading indicator
});
// Filter events by key
userManager.events
.where((event) => event.key.startsWith('user_'))
.listen((event) {
// Handle user-specific events
});
Force Sync Specific Keys
// Sync only specific items
await userManager.syncKeys(['user_1', 'user_2']);
π§ Troubleshooting
Common Issues
-
Background sync not working: Ensure WorkManager setup is correct and app has background permissions.
-
Encryption errors: Make sure the encryption key is consistent across app launches.
-
Memory issues: Reduce
batchSize
andmaxStorageSize
for memory-constrained devices. -
Sync conflicts: Implement proper conflict resolution strategies for your use case.
Debug Mode
Enable debug logging:
import 'package:flutter/foundation.dart';
// Debug events
if (kDebugMode) {
userManager.events.listen((event) {
print('SynQ Event: ${event.type} - ${event.key}');
});
}
π API Reference
SynqManager
Main manager class for synchronization operations.
Methods
Future<void> put(String key, T value, {Map<String, dynamic>? metadata})
- Store dataFuture<T?> get(String key)
- Retrieve dataFuture<void> update(String key, T value, {Map<String, dynamic>? metadata})
- Update dataFuture<void> delete(String key)
- Delete dataFuture<Map<String, T>> getAll()
- Get all dataFuture<void> sync()
- Manual syncFuture<void> syncKeys(List<String> keys)
- Sync specific keysFuture<void> resolveConflict(String key, ConflictResolutionStrategy strategy)
- Resolve conflicts
Properties
Stream<SynqEvent<T>> events
- All events streambool isReady
- Whether manager is readyConnectivityStatus connectivityStatus
- Current connectivitybool isSyncing
- Whether sync is in progressint pendingChangesCount
- Number of pending changesMap<String, DataConflict<T>> activeConflicts
- Active conflictsSyncStats syncStats
- Sync statistics
π€ Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Links
Made with β€οΈ for the Flutter community
Libraries
- synq_manager
- SynqManager - A powerful synchronization manager for Flutter apps