riverpod_offline_sync 1.0.2
riverpod_offline_sync: ^1.0.2 copied to clipboard
Production-ready offline-first sync engine for Flutter super apps with Riverpod integration
๐ฆ riverpod_offline_sync #
A production-ready offline-first sync engine for Flutter with Riverpod state management, queue orchestration, conflict resolution, Firebase integration, retry handling, connectivity monitoring, and built-in offline UI components.
Table of Contents #
- Features
- Dependencies
- Quick Start
- Architecture
- Core Concepts
- Providers
- Methods
- UI Components
- Firebase Integration
- Configuration
- Metrics & Analytics
- Retry System
- Connectivity Monitoring
- Utilities
- Examples
- Debugging
- FAQ
- License
โจ Features #
- ๐ Bi-directional sync (push + pull)
- ๐ฆ Persistent offline queue (Hive)
- โก Concurrent queue processing
- ๐ Smart retry with exponential backoff
- ๐ Connectivity-aware synchronization
- ๐ถ WiFi-only sync mode
- ๐ง Conflict resolution strategies
- ๐งพ Idempotency protection
- ๐ Sync metrics & analytics
- ๐ Debug inspection tools
- ๐จ Built-in UI widgets
- ๐ฅ Firebase integrations
- ๐งฉ Riverpod integration
- ๐ฑ Offline-first architecture
- ๐ Queue survives app restarts
- ๐ฏ Upload pause / resume / cancel
- ๐ Real-time sync progress streams
- ๐ Production-ready sync orchestration
Dependencies #
Add the following to your pubspec.yaml:
dependencies:
riverpod_offline_sync: ^0.1.0
flutter_riverpod: ^2.5.0
hive_flutter: ^1.1.0
connectivity_plus: ^5.0.0
synchronized: ^3.1.0
Then run:
flutter pub get
Quick Start #
1. Initialize in main() #
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_offline_sync/riverpod_offline_sync.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await OfflineSyncLayer.instance.initialize(
config: const SyncConfig(
autoSyncOnReconnect: true,
syncImmediately: true,
maxConcurrentOperations: 3,
),
);
runApp(const ProviderScope(child: MyApp()));
}
2. Register Operation Handlers #
OfflineSyncLayer.instance.registerOperationHandler(
'orders',
(data) async {
await api.createOrder(data);
},
);
OfflineSyncLayer.instance.registerOperationHandler(
'messages',
(data) async {
await api.sendMessage(data);
},
);
3. Wrap Your App #
MaterialApp(
home: ConnectivityBanner(
child: OfflineToast(
child: HomePage(),
),
),
)
4. Submit Offline Operations #
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: QueuePriority.high.value,
data: {
'product': 'Laptop',
'quantity': 1,
'price': 999.99,
},
);
๐ Architecture #
UI Widgets
โ
Riverpod Providers
โ
OfflineSyncLayer
โ
QueueManager
โ
Retry / Conflict / Connectivity Systems
โ
Persistence Layer (Hive)
โ
Backend APIs / Firebase / REST
๐ Package Structure #
lib/
โโโ core/
โโโ queue/
โโโ connectivity/
โโโ conflict/
โโโ firebase/
โโโ providers/
โโโ ui/
โโโ mixins/
โโโ utils/
โโโ riverpod_offline_sync.dart
Core Concepts #
Queue Priorities #
| Priority | Value | Use Cases |
|---|---|---|
| critical | 0 | Payments, KYC |
| high | 1 | Orders, Messages |
| normal | 2 | Profile updates |
| low | 3 | Analytics |
| background | 4 | Cache refresh |
Queue Categories #
enum QueueCategory {
uploads,
orders,
messages,
payments,
sync,
analytics,
media,
documents,
background,
}
Sync Strategies #
| Strategy | Description |
|---|---|
| auto | Automatic synchronization |
| manual | Manual user-triggered sync |
| background | Silent sync |
| pushOnly | Push local changes only |
| pullOnly | Pull remote changes only |
Conflict Resolution #
Supported strategies:
ConflictStrategy.serverWins
ConflictStrategy.clientWins
ConflictStrategy.merge
ConflictStrategy.lastWriteWins
ConflictStrategy.manualResolve
Example #
final resolver = ConflictResolver();
final resolved = await resolver.resolve(
local: localData,
remote: remoteData,
strategy: ConflictStrategy.merge,
);
Features #
- Deep merge support
- Nested map resolution
- List merging
- Duplicate removal
- Timestamp-based conflict handling
Providers #
Sync Providers #
final offlineSyncLayerProvider
final syncStateProvider
final syncProgressProvider
final syncMetricsProvider
final isSyncingProvider
final syncStatusTextProvider
Queue Providers #
final queueManagerProvider
final pendingItemsProvider
final pendingItemsCountProvider
final queueBreakdownProvider
Connectivity Providers #
final connectivityMonitorProvider
final connectivityStatusProvider
final isConnectedProvider
Methods #
OfflineSyncLayer #
Main singleton for sync orchestration.
await OfflineSyncLayer.instance.initialize();
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
data: {'key': 'value'},
);
await OfflineSyncLayer.instance.sync();
await OfflineSyncLayer.instance.clearQueue();
final pending =
await OfflineSyncLayer.instance.getPendingOperations();
QueueManager #
Handles queue orchestration.
final manager = QueueManager();
await manager.initialize();
await manager.enqueue(
category: 'orders',
priority: 1,
data: {},
);
await manager.processQueue(
maxConcurrent: 3,
);
await manager.retryFailed('item_id');
StorageQueue #
Queue-based Firebase Storage uploads.
final storageQueue = StorageQueue();
await storageQueue.uploadFile(
file: file,
path: 'uploads/image.jpg',
idempotencyKey: IdempotencyKey.generate(),
);
storageQueue.pauseUpload('key');
storageQueue.resumeUpload('key');
storageQueue.cancelUpload('key');
UI Components #
ConnectivityBanner #
Shows offline banner automatically.
ConnectivityBanner(
child: HomePage(),
)
SyncStatusIndicator #
Displays sync state.
SyncStatusIndicator(
showAsFloatingAction: true,
)
SyncProgressBar #
Displays queue progress.
SyncProgressBar(
showDetails: true,
)
OfflineToast #
Shows offline notifications.
OfflineToast(
child: HomePage(),
)
DebugPanel #
Powerful debug and inspection tool.
showModalBottomSheet(
context: context,
builder: (_) => const DebugPanel(),
);
Features #
- Queue inspection
- Sync metrics
- Connectivity status
- Retry operations
- Queue breakdown
- Manual sync controls
Firebase Integration #
Firestore #
syncLayer.registerOperationHandler(
'firestore_write',
(data) async {
await FirebaseFirestore.instance
.collection('todos')
.doc(data['id'])
.set(data);
},
);
Firebase Storage #
final storageQueue = StorageQueue();
await storageQueue.uploadFile(
file: file,
path: 'uploads/image.jpg',
idempotencyKey: IdempotencyKey.generate(),
);
Supports:
- Upload queueing
- Pause uploads
- Resume uploads
- Cancel uploads
- Progress tracking
Firebase Auth #
final auth = AuthPersistence();
await auth.signInWithEmail(
'email@example.com',
'password',
);
Configuration #
const config = SyncConfig(
autoSyncOnReconnect: true,
syncImmediately: true,
autoSyncInterval: Duration(minutes: 15),
maxRetries: 5,
initialRetryDelay: Duration(seconds: 2),
maxConcurrentOperations: 3,
enableMetrics: true,
enableDebugLogging: false,
syncOnWiFiOnly: false,
maxQueueSize: 1000,
);
Predefined Configurations #
SyncConfig.defaultConfig()
SyncConfig.aggressive()
SyncConfig.batteryFriendly()
SyncConfig.wifiOnly()
Metrics & Analytics #
Track:
- Total syncs
- Successful syncs
- Failed syncs
- Average sync duration
- Last sync time
- Success rate
Example:
final metrics = OfflineSyncLayer.instance.metrics;
print(metrics.successRatePercentage);
Retry System #
Features:
- Exponential backoff
- Delayed retry scheduling
- Retry tracking
- Configurable retry count
Example:
final delay =
BackoffCalculator.calculateNextRetry(3);
Connectivity Monitoring #
Automatically:
- Detects online/offline state
- Syncs on reconnect
- Supports WiFi-only mode
- Broadcasts connectivity changes
Example:
final connected =
OfflineSyncLayer.instance
.connectivityMonitor
.isConnected;
Utilities #
Idempotency Keys #
final key = IdempotencyKey.generate();
Logger #
OfflineLogger.isEnabled = true;
OfflineLogger.info('Sync started');
OfflineLogger.error('Sync failed');
OfflineLogger.debug('Queue processed');
Backoff Calculator #
final delay =
BackoffCalculator.calculateNextRetry(3);
Examples #
Offline Orders #
await submitOffline(
category: QueueCategory.orders.label,
priority: QueuePriority.high.value,
data: {
'product': 'Laptop',
'quantity': 1,
},
);
Chat Messages #
await submitOffline(
category: QueueCategory.messages.label,
priority: QueuePriority.high.value,
data: {
'message': 'Hello!',
},
);
Analytics Tracking #
await OfflineSyncLayer.instance.submitOperation(
category: 'analytics',
priority: QueuePriority.low.value,
data: {
'event_name': 'button_click',
'timestamp': DateTime.now().toIso8601String(),
},
);
Debugging #
Show Debug Panel #
showModalBottomSheet(
context: context,
builder: (_) => const DebugPanel(),
);
Manual Inspection #
final operations =
await OfflineSyncLayer.instance
.getPendingOperations();
final metrics =
OfflineSyncLayer.instance.metrics;
final isConnected =
OfflineSyncLayer.instance
.connectivityMonitor
.isConnected;
๐จ Troubleshooting #
Common issues and their solutions when using riverpod_offline_sync.
1. Hive Initialization Errors #
โ Error: HiveError: A Hive box with name 'offline_queue' already exists #
Cause #
Multiple initializations or hot reload during development.
Solution #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Hive before sync layer
await Hive.initFlutter();
// Then initialize offline sync
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
โ Error: HiveError: Hive has not been initialized. Did you forget to call Hive.initFlutter()? #
Solution #
await Hive.initFlutter();
await OfflineSyncLayer.instance.initialize();
โ Error: TypeAdapter for typeId 0 is already registered #
Solution #
// The package handles this internally with HiveRegistry.ensureRegistered()
// Make sure you're not manually registering adapters
2. Connectivity Listener Issues #
โ Error: connectivity_plus version mismatch #
Cause #
Different versions of connectivity_plus have different APIs.
Solution #
dependencies:
connectivity_plus: ^5.0.2
โ Error: StreamSubscription<ConnectivityResult> type mismatch #
Solution #
// Don't modify the connectivity_monitor.dart file
// The package handles both single and list results automatically
โ Error: Connectivity changes not detected #
Solution #
if (!OfflineSyncLayer.instance.connectivityMonitor.isConnected) {
print('Not connected to network');
}
final isConnected =
OfflineSyncLayer.instance
.connectivityMonitor
.isConnected;
3. Queue Stuck Processing #
โ Issue: Items in queue but never processed #
Option 1 โ Check if sync is enabled #
await OfflineSyncLayer.instance.sync();
Option 2 โ Check if handler is registered #
OfflineSyncLayer.instance.registerOperationHandler(
'your_category',
(data) async {
// Your processing logic
},
);
Option 3 โ Check retry scheduling #
await OfflineSyncLayer.instance.retryFailedOperation('item_id');
Option 4 โ Check queue size limits #
SyncConfig(
maxQueueSize: 2000,
);
Option 5 โ Check connectivity #
if (syncConfig.syncOnWiFiOnly) {
final isWifi =
await OfflineSyncLayer.instance
.connectivityMonitor
.isWifiConnected;
if (!isWifi) {
print('WiFi-only mode enabled');
}
}
4. Duplicate Operations #
โ Issue: Same operation submitted multiple times #
Solution 1 โ Use idempotency keys #
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
data: orderData,
idempotencyKey:
'unique_order_${orderData['id']}',
);
Solution 2 โ Generate UUID keys #
import 'package:uuid/uuid.dart';
final idempotencyKey = const Uuid().v4();
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
data: orderData,
idempotencyKey: idempotencyKey,
);
Solution 3 โ Use built-in generator #
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
data: orderData,
idempotencyKey: IdempotencyKey.generate(),
);
5. Provider Initialization Race Conditions #
โ Error: Null check operator used on a null value #
Solution #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
โ Error: OfflineSyncLayer not initialized #
Solution #
if (OfflineSyncLayer.instance.isInitialized) {
await OfflineSyncLayer.instance.submitOperation(...);
} else {
print('Sync layer not ready yet');
}
6. Firebase Integration Issues #
โ Error: [core/no-app] No Firebase App '[DEFAULT]' has been created #
Solution #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
โ Error: Firestore persistence enabled too late #
Solution #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await FirebaseFirestore.instance.settings =
const Settings(
persistenceEnabled: true,
);
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
7. Memory Leaks #
โ Issue: StreamControllers not disposed #
Solution #
await OfflineSyncLayer.instance.dispose();
8. Performance Issues #
โ Issue: Large queue affecting performance #
Solution 1 โ Limit concurrent operations #
SyncConfig(
maxConcurrentOperations: 3,
maxQueueSize: 500,
);
Solution 2 โ Use background priority #
await OfflineSyncLayer.instance.submitOperation(
category: 'analytics',
priority: QueuePriority.background.value,
data: analyticsData,
);
Solution 3 โ Enable WiFi-only mode #
SyncConfig(
syncOnWiFiOnly: true,
);
9. Debug Panel Not Showing #
โ Issue: Debug panel doesn't open or shows blank #
Solution #
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return const DebugPanel();
},
);
10. Common Setup Mistakes #
โ Mistake: Forgetting to register handlers #
Solution #
void main() async {
await OfflineSyncLayer.instance.initialize();
OfflineSyncLayer.instance.registerOperationHandler(
'orders',
(data) async {
// Process order
},
);
runApp(MyApp());
}
โ Mistake: Using providers before initialization #
Solution #
final pendingCountAsync =
ref.watch(pendingItemsCountProvider);
final pendingCount =
pendingCountAsync.valueOrNull ?? 0;
โ Mistake: Not handling async initialization in Riverpod #
Solution #
final pendingItems =
ref.watch(pendingItemsProvider)
.valueOrNull ?? [];
๐ Debug Checklist #
Use this checklist when troubleshooting:
- โ
OfflineSyncLayer.instance.isInitializedis true - โ Handlers registered for all categories
- โ Connectivity monitor working
- โ Queue has items
- โ No console errors
- โ Hive initialized
- โ Firebase initialized
- โ Permissions granted
- โ Not blocked by WiFi-only mode
- โ Unique idempotency keys used
๐ Getting Help #
Enable debug logging #
OfflineLogger.isEnabled = true;
Open debug panel #
showModalBottomSheet(
context: context,
builder: (_) => const DebugPanel(),
);
Inspect queue manually #
final pending =
await OfflineSyncLayer.instance
.getPendingOperations();
print('Pending operations: $pending');
Check sync metrics #
final metrics =
OfflineSyncLayer.instance.metrics;
print('Total syncs: ${metrics.totalSyncs}');
print('Failed syncs: ${metrics.failedSyncs}');
Open a GitHub issue with: #
- Error logs
- Steps to reproduce
- Flutter doctor output
- Package versions
This troubleshooting guide covers the most common issues and their solutions! ๐
FAQ #
Q: Does it work offline? #
A: Yes. Queue operations persist locally and sync automatically later.
Q: What happens if the app restarts? #
A: Queue data persists using Hive and resumes automatically.
Q: Does it support Firebase? #
A: Yes. Firestore, Storage, and Auth integrations are included.
Q: Can I use custom backends? #
A: Yes. Works with REST APIs, GraphQL, Firebase, or any backend.
Q: How are duplicates prevented? #
A: Idempotency keys prevent duplicate operations.
Q: Does it support large uploads? #
A: Yes. Includes pause, resume, cancel, and progress tracking.
Q: Is it Riverpod-only? #
A: Core sync system works independently, but Riverpod integration is included.
License #
MIT License โ see LICENSE file for details.
โค๏ธ Built For Offline-First Flutter Apps #
Reliable synchronization for:
- Super apps
- Delivery apps
- Chat apps
- POS systems
- CRM apps
- Warehouse systems
- Social apps
- Field-service apps
- Media upload apps
๐