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.
๐ 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, andSyncEventBuilder.
๐ฆ Installation
Add this to your pubspec.yaml:
dependencies:
sync_vault: ^0.0.1
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)โ
โโโโโโโโโโ
- Interceptor: Captures outgoing requests via
SyncVault.instance.post(), etc. - Queue Manager: Serializes requests to local storage (FIFO order).
- Network Watcher: Listens for connectivity changes via
connectivity_plus. - 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.
- Fork the repo
- Create your feature branch from
dev(git checkout -b feature/amazing-feature dev) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request against
dev
๐ค Author
Nabhodipta Garai (@brownboycodes)
๐ License
MIT ยฉ 2025 Nabhodipta Garai
Built with โค๏ธ for Flutter developers who work in the real world, where networks are unreliable.
Libraries
- sync_vault
- SyncVault - An offline-first data synchronization layer for Flutter.