flutter_sync_manager_pro

Offline-first data synchronization engine for Flutter applications.

pub package License: MIT

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

  1. Initialize early: Call syncEngine.initialize() in main() before runApp()
  2. Use priorities wisely: Only mark truly critical data as high/critical priority
  3. Handle conflicts: Choose an appropriate conflict resolution strategy for your use case
  4. Monitor stats: Display sync status in your UI so users know when data is syncing
  5. Clean up: Call syncEngine.dispose() when done (e.g., in app shutdown)
  6. Test offline: Always test your app in airplane mode
  7. 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

Changelog

See CHANGELOG.md for version history.

Libraries

flutter_sync_manager_pro
Offline-first data synchronization engine for Flutter.