sync_vault 1.0.0 copy "sync_vault: ^1.0.0" to clipboard
sync_vault: ^1.0.0 copied to clipboard

An offline-first data synchronization layer for Flutter apps. Automatically queues API requests when offline and syncs with exponential backoff.

SyncVault ๐Ÿ”„๐Ÿ—„๏ธ #

Stop writing connectivity logic.

SyncVault is an offline-first data synchronization layer for Flutter apps. It automatically queues API requests when the device is offline and syncs them with exponential backoff when the connection returns.

Designed for reliability in low-network environments.

pub package License: MIT

๐Ÿš€ Features #

  • Offline Queue: Automatically intercepts failed requests and stores them locally.
  • Auto-Sync: Detects network restoration and processes the queue in order (FIFO).
  • Persistence: Uses a pluggable storage adapter (Hive or SQLite).
  • Idempotency: Supports idempotency keys to prevent duplicate execution.
  • Configurable Retry: Exponential backoff with jitter, configurable max retries.
  • Dead Letter Queue: Failed requests (after max retries) are removed and can be handled via callback.
  • Status Stream: Real-time sync status for UI updates.
  • Event Bus: Track individual action lifecycles (queued โ†’ started โ†’ success/failed).
  • Flutter Widgets: Ready-to-use SyncVaultListener, SyncStatusBuilder, and SyncEventBuilder.

๐Ÿ“ฆ Installation #

Add this to your pubspec.yaml:

dependencies:
  sync_vault: ^1.0.0

Then run:

flutter pub get

๐Ÿ›  Usage #

1. Initialize #

Initialize SyncVault in your main.dart before running the app:

import 'package:sync_vault/sync_vault.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Get storage directory (required for Hive)
  final dir = await getApplicationDocumentsDirectory();

  await SyncVault.init(
    config: SyncVaultConfig(
      baseUrl: 'https://api.myapp.com',
      storagePath: dir.path,
      maxRetries: 3,
    ),
  );

  runApp(MyApp());
}

2. Make Offline-Safe Requests #

Instead of calling Dio or Http directly, use the SyncVault client:

// POST request
final response = await SyncVault.instance.post(
  '/jobs/complete',
  data: {'jobId': 123, 'status': 'DONE'},
  idempotencyKey: 'job_123_complete', // Prevents duplicate execution
);

if (response.isQueued) {
  print("Saved offline! Will sync later.");
} else if (response.isSent) {
  print("Sent immediately! Status: ${response.response?.statusCode}");
}

// GET, PUT, DELETE, PATCH are also available
await SyncVault.instance.get('/users');
await SyncVault.instance.put('/users/123', data: {...});
await SyncVault.instance.delete('/users/123');
await SyncVault.instance.patch('/users/123', data: {...});

3. Listen to Sync Status #

Perfect for showing a "Syncing..." indicator in your UI:

SyncVault.instance.statusStream.listen((status) {
  switch (status) {
    case SyncStatus.syncing:
      showToast("Uploading offline data...");
    case SyncStatus.synced:
      showToast("All caught up!");
    case SyncStatus.pending:
      showToast("${await SyncVault.instance.pendingCount} items waiting");
    case SyncStatus.error:
      showToast("Sync error occurred");
  }
});

4. Track Individual Actions (Event Bus) #

Listen to specific job completions โ€” perfect for updating UI when background sync finishes:

// Listen to all events
SyncVault.instance.onEvent.listen((event) {
  print('Action ${event.id}: ${event.status}');

  if (event.isSuccess) {
    // Action completed successfully
    print('Response: ${event.response}');
  } else if (event.isPermanentFailure) {
    // Action failed permanently (dead letter)
    print('Error: ${event.error}');
  }
});

// Filter events for a specific action
SyncVault.instance.eventsFor('action-id').listen((event) {
  // Only events for this action
});

// Filter by idempotency key
SyncVault.instance.eventsForKey('my-idempotency-key').listen((event) {
  // Only events with this key
});

Event Statuses:

Status Description
queued Action was saved to offline queue
started Worker began processing this action
success Server returned 2xx response
failed Action failed (will retry)
deadLetter Action permanently failed after max retries

5. Manual Sync #

Trigger sync manually if needed:

final successCount = await SyncVault.instance.processQueue();
print('Synced $successCount items');

6. Check Pending Actions #

// Get count
final count = await SyncVault.instance.pendingCount;

// Get all pending actions
final actions = await SyncVault.instance.getPendingActions();

// Clear all pending (use with caution)
await SyncVault.instance.clearQueue();

๐ŸŽจ Flutter Widgets #

SyncVault provides ready-to-use widgets for common UI patterns:

SyncVaultListener #

Listens to sync events and calls callbacks โ€” wrap your screen to react to background sync:

SyncVaultListener(
  // Called when any action completes successfully
  onSuccess: (event) {
    if (event.id == myActionId) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Synced!')),
      );
    }
  },
  // Called when an action permanently fails
  onDeadLetter: (event) {
    showError('Sync failed: ${event.error}');
  },
  // Optional: filter by action ID or idempotency key
  actionId: 'specific-action-id',
  idempotencyKey: 'specific-key',
  child: YourScreen(),
)

SyncStatusBuilder #

Rebuilds automatically when sync status changes:

SyncStatusBuilder(
  builder: (context, status) {
    return Icon(
      status == SyncStatus.syncing
          ? Icons.sync
          : Icons.cloud_done,
      color: status == SyncStatus.error
          ? Colors.red
          : Colors.green,
    );
  },
)

SyncEventBuilder #

Rebuilds when events occur for a specific action โ€” great for per-item sync indicators:

SyncEventBuilder(
  actionId: myActionId,
  builder: (context, lastEvent) {
    if (lastEvent == null) {
      return Icon(Icons.cloud_upload); // Queued
    }
    if (lastEvent.status == SyncEventStatus.started) {
      return CircularProgressIndicator(); // Syncing
    }
    if (lastEvent.isSuccess) {
      return Icon(Icons.check, color: Colors.green);
    }
    return Icon(Icons.error, color: Colors.red);
  },
)

๐Ÿ—๏ธ Architecture #

SyncVault follows a Store-and-Forward architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Your App      โ”‚
โ”‚   (Request)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   SyncVault     โ”‚
โ”‚   (Intercept)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
    โ”‚ Online? โ”‚
    โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
         โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”
   Yes       No
    โ”‚         โ”‚
    โ–ผ         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Dio  โ”‚ โ”‚ Queue โ”‚
โ”‚ (Send)โ”‚ โ”‚(Store)โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”˜
              โ”‚
              โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚  Hive  โ”‚
         โ”‚(Persist)โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚
    On Network Restore
              โ”‚
              โ–ผ
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ”‚ Worker โ”‚
         โ”‚(Process)โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
  1. Interceptor: Captures outgoing requests via SyncVault.instance.post(), etc.
  2. Queue Manager: Serializes requests to local storage (FIFO order).
  3. Network Watcher: Listens for connectivity changes via connectivity_plus.
  4. Worker: Dequeues requests and retries with exponential backoff.

โš™๏ธ Configuration #

SyncVaultConfig(
  // Required
  baseUrl: 'https://api.example.com',

  // Optional: Custom storage path for Hive
  storagePath: '/path/to/storage',

  // Optional: Maximum retry attempts (default: 3)
  maxRetries: 5,

  // Optional: Custom Dio instance
  dio: myCustomDio,

  // Optional: Timeouts
  connectTimeout: Duration(seconds: 30),
  receiveTimeout: Duration(seconds: 30),

  // Optional: Custom storage adapter
  storage: MyCustomStorageAdapter(),
)

๐Ÿ’พ Storage Adapters #

SyncVault supports multiple storage backends out of the box:

Hive (Default) #

Hive is the default storage adapter - fast, lightweight, and perfect for most use cases.

await SyncVault.init(
  config: SyncVaultConfig(
    baseUrl: 'https://api.example.com',
    storagePath: dir.path, // Required for Hive
  ),
);

SQLite #

For apps that prefer relational databases or need more complex queries:

import 'package:sync_vault/sync_vault.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Create SQLite adapter
  final sqliteAdapter = SqfliteStorageAdapter();

  await SyncVault.init(
    config: SyncVaultConfig(
      baseUrl: 'https://api.example.com',
      storage: sqliteAdapter, // Use SQLite instead of Hive
    ),
  );

  runApp(MyApp());
}

You can also specify a custom database path:

final sqliteAdapter = SqfliteStorageAdapter(
  databasePath: '/custom/database/path',
);

๐Ÿ”Œ Custom Storage Adapter #

Implement StorageAdapter to use your own persistence layer:

class MyStorageAdapter implements StorageAdapter {
  @override
  Future<void> init() async { ... }

  @override
  Future<void> saveAction(SyncAction action) async { ... }

  @override
  Future<void> removeAction(String id) async { ... }

  @override
  Future<List<SyncAction>> getAllActions() async { ... }

  @override
  Future<void> clear() async { ... }

  @override
  Future<void> dispose() async { ... }

  @override
  Future<int> get count async { ... }
}

๐Ÿงช Testing #

SyncVault is designed to be testable. Reset between tests:

tearDown(() async {
  await SyncVault.reset();
});

๐Ÿ“ฑ Example #

Check out the example directory for a complete Todo app demonstrating all features.

cd example
flutter run

๐Ÿ”’ Idempotency #

Use idempotency keys to prevent duplicate operations when retrying:

await SyncVault.instance.post(
  '/payments',
  data: {'amount': 100},
  idempotencyKey: 'payment_${orderId}_${timestamp}',
);

If the same idempotency key is already in the queue, SyncVault will handle it appropriately.

๐Ÿ“Š Retry Behavior #

Scenario Behavior
Network error Retry with backoff
Timeout Retry with backoff
5xx Server error Retry with backoff
429 Rate limited Retry with backoff
4xx Client error Fail permanently
Max retries exceeded Remove (dead letter)

๐Ÿค Contributing #

PRs are welcome! Please read the contributing guidelines before submitting.

  1. Fork the repo
  2. Create your feature branch from dev (git checkout -b feature/amazing-feature dev)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request against dev

๐Ÿ‘ค Author #

Nabhodipta Garai (@brownboycodes)

"Buy Me A Coffee"

๐Ÿ“„ License #

MIT ยฉ 2025 Nabhodipta Garai


Built with โค๏ธ for Flutter developers who work in the real world, where networks are unreliable.

7
likes
160
points
90
downloads

Publisher

verified publisherbrownboycodes.com

Weekly Downloads

An offline-first data synchronization layer for Flutter apps. Automatically queues API requests when offline and syncs with exponential backoff.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

connectivity_plus, dio, equatable, flutter, hive, path, path_provider, sqflite, uuid

More

Packages that depend on sync_vault