synq_manager 1.0.10
synq_manager: ^1.0.10 copied to clipboard
A powerful synchronization manager for Flutter apps with secure local storage, real-time state management, and background cloud sync capabilities.
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 #
- Socket.io Style Events: Intuitive event handling with
onInit,onCreate,onUpdate,onDelete - 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 configurable batch sizes
- ๐ง Customizable: Flexible configuration for different use cases
- ๐ Event-driven: Real-time event streams for UI updates
- ๐ Local-first: Works offline, syncs when online
- โญ Builder Pattern: Quick setup with fluent API
๐ 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.xmlinside<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() async {
// Method 1: Builder Pattern - Quick Setup
_listeners = await userManager.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);
});
})
.onSyncStart(() {
setState(() => _syncing = true);
})
.onSyncComplete(() {
setState(() => _syncing = false);
_showMessage('Sync completed! โ
');
})
.onError((error) {
setState(() => _syncing = false);
_showError('Sync failed: $error');
})
.start(); // Don't forget to call start()!
}
// Method 2: Fluent Interface - More Control
void _setupFluentListeners() {
_listeners = userManager.on()
..onInit((allProfiles) {
setState(() => _profiles = allProfiles.values.toList());
})
..onChange((key, profile, action) {
// Single handler for all changes
print('$action: $key');
switch (action) {
case 'create':
setState(() => _profiles.add(profile!));
break;
case 'update':
setState(() {
final index = _profiles.indexWhere((p) => p.id == key);
if (index != -1) _profiles[index] = profile!;
});
break;
case 'delete':
setState(() => _profiles.removeWhere((p) => p.id == key));
break;
}
})
..onSyncStart(() => setState(() => _syncing = true))
..onSyncComplete(() => setState(() => _syncing = false))
..onConnectionChange((isOnline) {
_showMessage(isOnline ? 'Back online! ๐' : 'Gone offline ๐ด');
});
}
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 |
|---|---|---|---|
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 |
onChange(callback) |
Any data change | Key + data + action | Handle all changes in one place |
onSyncStart() |
Sync begins | - | Show loading |
onSyncComplete() |
Sync ends | - | Hide loading |
onError(callback) |
Error occurs | Error object | Show error message |
Key Benefits of Socket.io Style:
- ๐ Less Code: No need to manually reload all data
- โก Better Performance: Only changed data is provided
- ๐ฏ More Intuitive: Familiar API for web developers
- ๐ Auto UI Updates: UI automatically reflects changes
๐ง 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
batchSizeandmaxStorageSizefor 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 #
- Documentation
- Issues
- Changelog
Made with โค๏ธ for the Flutter community