synq_manager 1.0.3 copy "synq_manager: ^1.0.3" to clipboard
synq_manager: ^1.0.3 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 #

pub package License Flutter

A powerful synchronization manager for Flutter apps with secure local storage, real-time state management, and background cloud sync capabilities.

โœจ Features #

  • ๐Ÿ”„ 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

๐Ÿš€ 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: ^1.0.0

Run:

flutter pub get

๐Ÿ› ๏ธ WorkManager Setup #

SynQ Manager uses WorkManager for background synchronization. Follow these platform-specific setup instructions:

Android Setup #

  1. Minimum SDK Version: Add to android/app/build.gradle:
android {
    compileSdkVersion 34
    
    defaultConfig {
        minSdkVersion 23  // WorkManager requires API 23+
        targetSdkVersion 34
    }
}
  1. 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" />
  1. 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 #

  1. Minimum iOS Version: Update ios/Podfile:
platform :ios, '12.0'  # WorkManager requires iOS 12.0+
  1. Background Modes: Add to ios/Runner/Info.plist:
<key>UIBackgroundModes</key>
<array>
    <string>background-processing</string>
    <string>background-fetch</string>
</array>
  1. 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,
  );
}

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();
  }
}

โš™๏ธ 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 #

  1. Background sync not working: Ensure WorkManager setup is correct and app has background permissions.

  2. Encryption errors: Make sure the encryption key is consistent across app launches.

  3. Memory issues: Reduce batchSize and maxStorageSize for memory-constrained devices.

  4. 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 data
  • Future<T?> get(String key) - Retrieve data
  • Future<void> update(String key, T value, {Map<String, dynamic>? metadata}) - Update data
  • Future<void> delete(String key) - Delete data
  • Future<Map<String, T>> getAll() - Get all data
  • Future<void> sync() - Manual sync
  • Future<void> syncKeys(List<String> keys) - Sync specific keys
  • Future<void> resolveConflict(String key, ConflictResolutionStrategy strategy) - Resolve conflicts

Properties

  • Stream<SynqEvent<T>> events - All events stream
  • bool isReady - Whether manager is ready
  • ConnectivityStatus connectivityStatus - Current connectivity
  • bool isSyncing - Whether sync is in progress
  • int pendingChangesCount - Number of pending changes
  • Map<String, DataConflict<T>> activeConflicts - Active conflicts
  • SyncStats 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.


Made with โค๏ธ for the Flutter community

3
likes
0
points
22
downloads

Publisher

verified publisherahmetaydin.dev

Weekly Downloads

A powerful synchronization manager for Flutter apps with secure local storage, real-time state management, and background cloud sync capabilities.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

connectivity_plus, flutter, hive_plus_secure, isar_plus_flutter_libs, meta, path_provider, workmanager

More

Packages that depend on synq_manager