riverpod_offline_sync 1.0.3
riverpod_offline_sync: ^1.0.3 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 with Firebase
- Complete Firebase Integration
- Complete Service Class Example
- Complete Todo App Example
- Architecture
- Core Concepts
- Providers
- Methods
- UI Components
- Configuration
- Metrics & Analytics
- Retry System
- Connectivity Monitoring
- Utilities
- Examples
- Debugging
- Troubleshooting
- 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
- ๐ฅ Full Firebase integrations (Firestore, Storage, Auth)
- ๐งฉ 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
firebase_core: ^2.24.0
cloud_firestore: ^4.17.0
firebase_storage: ^11.6.0
firebase_auth: ^4.17.0
hive_flutter: ^1.1.0
connectivity_plus: ^5.0.0
synchronized: ^3.1.0
http: ^1.1.0
Then run:
flutter pub get
Quick Start with Firebase #
1. Complete Setup in main() #
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_offline_sync/riverpod_offline_sync.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase
await Firebase.initializeApp();
// Enable Firestore native offline persistence
await FirebaseFirestore.instance.settings =
const Settings(persistenceEnabled: true);
// Initialize offline sync layer
await OfflineSyncLayer.instance.initialize(
config: const SyncConfig(
autoSyncOnReconnect: true,
syncImmediately: true,
maxConcurrentOperations: 3,
enableMetrics: true,
enableDebugLogging: false,
),
);
// Register ALL operation handlers
_registerHandlers();
runApp(const ProviderScope(child: MyApp()));
}
2. Register All Operation Handlers #
void _registerHandlers() {
final sync = OfflineSyncLayer.instance;
// โ
CREATE / SET document handler
sync.registerOperationHandler('firestore_set', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.set(Map<String, dynamic>.from(data['payload']));
});
// โ
UPDATE document handler
sync.registerOperationHandler('firestore_update', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.update(Map<String, dynamic>.from(data['payload']));
});
// โ
DELETE document handler
sync.registerOperationHandler('firestore_delete', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.delete();
});
// โ
Batch write handler
sync.registerOperationHandler('firestore_batch', (data) async {
final batch = FirebaseFirestore.instance.batch();
final writes = List<Map<String, dynamic>>.from(data['writes']);
for (final write in writes) {
final docRef = FirebaseFirestore.instance
.collection(write['collection'])
.doc(write['docId']);
switch (write['type']) {
case 'set':
batch.set(docRef, write['data']);
break;
case 'update':
batch.update(docRef, write['data']);
break;
case 'delete':
batch.delete(docRef);
break;
}
}
await batch.commit();
});
// โ
Custom REST API handler
sync.registerOperationHandler('api_request', (data) async {
final response = await http.post(
Uri.parse(data['url']),
body: data['body'],
headers: data['headers'],
);
if (response.statusCode != 200) {
throw Exception('API request failed: ${response.statusCode}');
}
return response.body;
});
// โ
Analytics handler (low priority)
sync.registerOperationHandler('analytics', (data) async {
await AnalyticsService.trackEvent(
data['event_name'],
properties: data['properties'],
);
});
}
3. Wrap Your App #
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Offline-First App',
home: ConnectivityBanner(
child: OfflineToast(
child: HomePage(),
),
),
);
}
}
4. Submit Offline Operations #
// โ
CREATE document (works offline)
Future<void> createUser(String userId, String name, String email) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_set',
priority: QueuePriority.high.value,
idempotencyKey: 'set_user_${userId}_${DateTime.now().millisecondsSinceEpoch}',
data: {
'collection': 'users',
'docId': userId,
'payload': {
'name': name,
'email': email,
'createdAt': DateTime.now().toIso8601String(),
'updatedAt': DateTime.now().toIso8601String(),
},
},
);
}
// โ
UPDATE document (works offline)
Future<void> updateUser(String userId, Map<String, dynamic> updates) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_update',
priority: QueuePriority.normal.value,
idempotencyKey: IdempotencyKey.generate(),
data: {
'collection': 'users',
'docId': userId,
'payload': {
...updates,
'updatedAt': DateTime.now().toIso8601String(),
},
},
);
}
// โ
DELETE document (works offline)
Future<void> deleteUser(String userId) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_delete',
priority: QueuePriority.high.value,
idempotencyKey: 'delete_user_${userId}_${DateTime.now().millisecondsSinceEpoch}',
data: {
'collection': 'users',
'docId': userId,
},
);
}
Complete Firebase Integration #
Firestore Operations #
class OfflineFirestoreService {
static Future<void> submitOperation({
required String category,
required Map<String, dynamic> data,
QueuePriority priority = QueuePriority.normal,
String? customIdempotencyKey,
}) async {
await OfflineSyncLayer.instance.submitOperation(
category: category,
priority: priority.value,
idempotencyKey: customIdempotencyKey ?? IdempotencyKey.generate(),
data: data,
);
}
static Future<void> setDocument({
required String collection,
required String docId,
required Map<String, dynamic> data,
}) async {
await submitOperation(
category: 'firestore_set',
priority: QueuePriority.high,
data: {'collection': collection, 'docId': docId, 'payload': data},
);
}
static Future<void> updateDocument({
required String collection,
required String docId,
required Map<String, dynamic> updates,
}) async {
await submitOperation(
category: 'firestore_update',
priority: QueuePriority.normal,
data: {'collection': collection, 'docId': docId, 'payload': updates},
);
}
static Future<void> deleteDocument({
required String collection,
required String docId,
}) async {
await submitOperation(
category: 'firestore_delete',
priority: QueuePriority.high,
data: {'collection': collection, 'docId': docId},
);
}
static Future<void> batchWrite({
required List<Map<String, dynamic>> operations,
}) async {
await submitOperation(
category: 'firestore_batch',
priority: QueuePriority.high,
data: {'writes': operations},
);
}
}
// Usage
await OfflineFirestoreService.setDocument(
collection: 'products',
docId: 'prod_001',
data: {'name': 'Laptop', 'price': 999},
);
await OfflineFirestoreService.updateDocument(
collection: 'products',
docId: 'prod_001',
updates: {'price': 899},
);
await OfflineFirestoreService.batchWrite(
operations: [
{'type': 'update', 'collection': 'products', 'docId': 'prod_001', 'data': {'stock': 5}},
{'type': 'set', 'collection': 'orders', 'docId': 'order_001', 'data': {'productId': 'prod_001'}},
],
);
Firebase Storage Uploads #
final storageQueue = StorageQueue();
// Upload a file with full queue support
Future<void> uploadUserPhoto(File file, String userId) async {
final idempotencyKey = IdempotencyKey.generate();
await storageQueue.uploadFile(
file: file,
path: 'uploads/$userId/photo.jpg',
idempotencyKey: idempotencyKey,
);
// Control the upload
storageQueue.pauseUpload(idempotencyKey);
storageQueue.resumeUpload(idempotencyKey);
storageQueue.cancelUpload(idempotencyKey);
// Track progress
storageQueue.progressStream.listen((progress) {
if (progress.key == idempotencyKey) {
print('Upload progress: ${progress.percentage}%');
print('Uploaded: ${progress.bytesUploaded}/${progress.totalBytes}');
}
});
}
// Upload multiple files
Future<void> uploadMultipleFiles(List<File> files, String userId) async {
for (final file in files) {
await storageQueue.uploadFile(
file: file,
path: 'uploads/$userId/${DateTime.now().millisecondsSinceEpoch}.jpg',
idempotencyKey: IdempotencyKey.generate(),
);
}
}
Firebase Auth Persistence #
final auth = AuthPersistence();
// Sign in (works offline - stores credentials)
await auth.signInWithEmail('email@example.com', 'password');
// Check auth state
final user = auth.currentUser;
if (user != null) {
print('Signed in as: ${user.email}');
}
Real-time Firestore + Offline Queue #
// Provider for real-time Firestore data
final usersProvider = StreamProvider.autoDispose<List<User>>((ref) {
return FirebaseFirestore.instance
.collection('users')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => User.fromFirestore(doc))
.toList());
});
class UserListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(usersProvider);
final pendingCount = ref.watch(pendingItemsCountProvider).valueOrNull ?? 0;
final isSyncing = ref.watch(isSyncingProvider);
return Scaffold(
appBar: AppBar(
title: Text('Users'),
actions: [
if (pendingCount > 0)
Stack(
children: [
Icon(Icons.sync),
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(minWidth: 16, minHeight: 16),
child: Text(
'$pendingCount',
style: TextStyle(color: Colors.white, fontSize: 10),
textAlign: TextAlign.center,
),
),
),
],
),
],
),
body: Stack(
children: [
usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
subtitle: Text(users[index].email),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
await OfflineFirestoreService.deleteDocument(
collection: 'users',
docId: users[index].id,
);
},
),
);
},
),
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(child: Text('Error: $err')),
),
if (isSyncing)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(),
),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
await OfflineFirestoreService.setDocument(
collection: 'users',
docId: 'user_${DateTime.now().millisecondsSinceEpoch}',
data: {
'name': 'New User',
'email': 'new@example.com',
'createdAt': DateTime.now().toIso8601String(),
},
);
},
),
);
}
}
Complete Service Class Example #
class OfflineSyncService {
static Future<void> initialize() async {
await Firebase.initializeApp();
await FirebaseFirestore.instance.settings =
const Settings(persistenceEnabled: true);
await OfflineSyncLayer.instance.initialize(
config: SyncConfig.defaultConfig(),
);
_registerHandlers();
}
static void _registerHandlers() {
final sync = OfflineSyncLayer.instance;
sync.registerOperationHandler('firestore_set', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.set(data['payload']);
});
sync.registerOperationHandler('firestore_update', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.update(data['payload']);
});
sync.registerOperationHandler('firestore_delete', (data) async {
await FirebaseFirestore.instance
.collection(data['collection'])
.doc(data['docId'])
.delete();
});
}
static Future<void> create(String collection, String id, Map<String, dynamic> data) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_set',
priority: QueuePriority.high.value,
idempotencyKey: IdempotencyKey.generate(),
data: {'collection': collection, 'docId': id, 'payload': data},
);
}
static Future<void> update(String collection, String id, Map<String, dynamic> updates) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_update',
priority: QueuePriority.normal.value,
idempotencyKey: IdempotencyKey.generate(),
data: {'collection': collection, 'docId': id, 'payload': updates},
);
}
static Future<void> delete(String collection, String id) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_delete',
priority: QueuePriority.high.value,
idempotencyKey: IdempotencyKey.generate(),
data: {'collection': collection, 'docId': id},
);
}
static Future<void> forceSync() async => OfflineSyncLayer.instance.sync();
static Future<int> getPendingCount() async {
final pending = await OfflineSyncLayer.instance.getPendingOperations();
return pending.length;
}
static Future<void> clearQueue() async => OfflineSyncLayer.instance.clearQueue();
static Future<void> retryFailed(String id) async =>
OfflineSyncLayer.instance.retryFailedOperation(id);
}
Complete Todo App Example #
Todo Model #
class Todo {
final String id;
final String title;
final bool completed;
final DateTime createdAt;
final DateTime? updatedAt;
Todo({
required this.id,
required this.title,
this.completed = false,
required this.createdAt,
this.updatedAt,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'completed': completed,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
};
factory Todo.fromJson(Map<String, dynamic> json) => Todo(
id: json['id'],
title: json['title'],
completed: json['completed'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : null,
);
factory Todo.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return Todo(
id: doc.id,
title: data['title'],
completed: data['completed'],
createdAt: (data['createdAt'] as Timestamp).toDate(),
updatedAt: data['updatedAt'] != null
? (data['updatedAt'] as Timestamp).toDate()
: null,
);
}
Todo copyWith({String? title, bool? completed}) => Todo(
id: id,
title: title ?? this.title,
completed: completed ?? this.completed,
createdAt: createdAt,
updatedAt: DateTime.now(),
);
}
Todo Service #
class TodoService {
static Future<void> addTodo(Todo todo) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_set',
priority: QueuePriority.high.value,
idempotencyKey: 'todo_${todo.id}_${DateTime.now().millisecondsSinceEpoch}',
data: {
'collection': 'todos',
'docId': todo.id,
'payload': todo.toJson(),
},
);
}
static Future<void> updateTodo(Todo todo) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_update',
priority: QueuePriority.normal.value,
idempotencyKey: IdempotencyKey.generate(),
data: {
'collection': 'todos',
'docId': todo.id,
'payload': {
'completed': todo.completed,
'updatedAt': DateTime.now().toIso8601String(),
},
},
);
}
static Future<void> deleteTodo(String todoId) async {
await OfflineSyncLayer.instance.submitOperation(
category: 'firestore_delete',
priority: QueuePriority.high.value,
idempotencyKey: 'delete_todo_${todoId}_${DateTime.now().millisecondsSinceEpoch}',
data: {
'collection': 'todos',
'docId': todoId,
},
);
}
static Stream<List<Todo>> streamTodos() {
return FirebaseFirestore.instance
.collection('todos')
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => Todo.fromFirestore(doc))
.toList());
}
static Future<void> forceSync() async => OfflineSyncLayer.instance.sync();
}
Todo List Page #
final todoListProvider = StreamProvider.autoDispose<List<Todo>>((ref) {
return TodoService.streamTodos();
});
class TodoListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final todosAsync = ref.watch(todoListProvider);
final pendingCount = ref.watch(pendingItemsCountProvider).valueOrNull ?? 0;
final isSyncing = ref.watch(isSyncingProvider);
return Scaffold(
appBar: AppBar(
title: Text('Offline Todo'),
actions: [
if (pendingCount > 0 || isSyncing)
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Center(
child: Row(
children: [
if (isSyncing)
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
if (pendingCount > 0)
Padding(
padding: EdgeInsets.only(left: 4),
child: Text('$pendingCount'),
),
],
),
),
),
IconButton(
icon: Icon(Icons.sync),
onPressed: () async {
await TodoService.forceSync();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Syncing...')),
);
},
),
],
),
body: Stack(
children: [
todosAsync.when(
data: (todos) {
if (todos.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No todos yet', style: TextStyle(color: Colors.grey)),
SizedBox(height: 8),
Text('Add your first todo!', style: TextStyle(fontSize: 12)),
],
),
);
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return CheckboxListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed ? TextDecoration.lineThrough : null,
),
),
value: todo.completed,
onChanged: (completed) async {
await TodoService.updateTodo(todo.copyWith(completed: completed));
},
secondary: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () async => TodoService.deleteTodo(todo.id),
),
);
},
);
},
loading: () => Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.red),
SizedBox(height: 16),
Text('Error loading todos: $err'),
SizedBox(height: 8),
ElevatedButton(
onPressed: () => ref.invalidate(todoListProvider),
child: Text('Retry'),
),
],
),
),
),
if (isSyncing)
Positioned(
top: 0,
left: 0,
right: 0,
child: LinearProgressIndicator(),
),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
final newTodo = Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: 'New Task ${DateTime.now().hour}:${DateTime.now().minute}',
completed: false,
createdAt: DateTime.now(),
);
await TodoService.addTodo(newTodo);
},
),
);
}
}
๐๏ธ Architecture #
UI Widgets
โ
Riverpod Providers (Real-time data)
โ
OfflineSyncLayer (Queue management)
โ
QueueManager (Orchestration)
โ
Retry / Conflict / Connectivity Systems
โ
Persistence Layer (Hive for queue + Firestore for data)
โ
Backend APIs / Firebase
๐ Package Structure #
lib/
โโโ core/
โโโ queue/
โโโ connectivity/
โโโ conflict/
โโโ firebase/
โ โโโ firestore_service.dart
โ โโโ storage_queue.dart
โ โโโ auth_persistence.dart
โโโ 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 #
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 #
// Initialize
await OfflineSyncLayer.instance.initialize();
// Submit operation
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
idempotencyKey: IdempotencyKey.generate(),
data: {'key': 'value'},
);
// Manual sync
await OfflineSyncLayer.instance.sync();
// Clear queue
await OfflineSyncLayer.instance.clearQueue();
// Get pending operations
final pending = await OfflineSyncLayer.instance.getPendingOperations();
// Retry failed operation
await OfflineSyncLayer.instance.retryFailedOperation('item_id');
// Check initialization
if (OfflineSyncLayer.instance.isInitialized) {
// Safe to use
}
// Dispose
await OfflineSyncLayer.instance.dispose();
QueueManager #
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 #
final storageQueue = StorageQueue();
// Upload file
await storageQueue.uploadFile(
file: file,
path: 'uploads/image.jpg',
idempotencyKey: IdempotencyKey.generate(),
);
// Control upload
storageQueue.pauseUpload('key');
storageQueue.resumeUpload('key');
storageQueue.cancelUpload('key');
// Track progress
storageQueue.progressStream.listen((progress) {
print('Progress: ${progress.percentage}%');
});
UI Components #
ConnectivityBanner #
ConnectivityBanner(
child: HomePage(),
)
SyncStatusIndicator #
SyncStatusIndicator(
showAsFloatingAction: true,
)
SyncProgressBar #
SyncProgressBar(
showDetails: true,
)
OfflineToast #
OfflineToast(
child: HomePage(),
)
DebugPanel #
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const DebugPanel(),
);
Features: Queue inspection, sync metrics, connectivity status, retry operations, queue breakdown, manual sync controls.
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() // Balanced
SyncConfig.aggressive() // Fast sync, more battery
SyncConfig.batteryFriendly() // Less frequent sync
SyncConfig.wifiOnly() // Only sync on WiFi
Metrics & Analytics #
final metrics = OfflineSyncLayer.instance.metrics;
print('Success rate: ${metrics.successRatePercentage}%');
print('Total syncs: ${metrics.totalSyncs}');
print('Failed syncs: ${metrics.failedSyncs}');
print('Avg duration: ${metrics.averageSyncDuration}');
Retry System #
final delay = BackoffCalculator.calculateNextRetry(attempt: 3);
// Returns: Duration(seconds: 8) for attempt 3 with base delay 2
Features: Exponential backoff, delayed retry scheduling, retry tracking, configurable retry count.
Connectivity Monitoring #
final connected = OfflineSyncLayer.instance.connectivityMonitor.isConnected;
final isWifi = await OfflineSyncLayer.instance.connectivityMonitor.isWifiConnected;
// Listen to changes
OfflineSyncLayer.instance.connectivityMonitor.onConnectivityChanged.listen((status) {
print('Connectivity: $status');
});
Utilities #
Idempotency Keys #
// Auto-generate
final key = IdempotencyKey.generate();
// Custom key
final customKey = 'user_${userId}_action_${timestamp}';
Logger #
OfflineLogger.isEnabled = true;
OfflineLogger.info('Sync started');
OfflineLogger.error('Sync failed: $error');
OfflineLogger.debug('Queue processed: ${items.length} items');
Backoff Calculator #
final delay = BackoffCalculator.calculateNextRetry(3);
// Exponential backoff: 2^3 = 8 seconds
Examples #
Offline Orders #
await OfflineSyncLayer.instance.submitOperation(
category: QueueCategory.orders.label,
priority: QueuePriority.high.value,
idempotencyKey: IdempotencyKey.generate(),
data: {
'product': 'Laptop',
'quantity': 1,
'price': 999.99,
'userId': currentUser.id,
},
);
Chat Messages #
await OfflineSyncLayer.instance.submitOperation(
category: QueueCategory.messages.label,
priority: QueuePriority.high.value,
idempotencyKey: IdempotencyKey.generate(),
data: {
'message': 'Hello!',
'recipientId': 'user456',
'timestamp': DateTime.now().toIso8601String(),
},
);
Analytics Tracking #
await OfflineSyncLayer.instance.submitOperation(
category: 'analytics',
priority: QueuePriority.low.value,
idempotencyKey: IdempotencyKey.generate(),
data: {
'event_name': 'button_click',
'properties': {
'button': 'add_todo',
'screen': 'home',
},
'timestamp': DateTime.now().toIso8601String(),
},
);
Debugging #
Enable Debug Logging #
OfflineLogger.isEnabled = true;
Show Debug Panel #
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) => const DebugPanel(),
);
Manual Inspection #
// Get pending operations
final operations = await OfflineSyncLayer.instance.getPendingOperations();
print('Pending: ${operations.length}');
// Get metrics
final metrics = OfflineSyncLayer.instance.metrics;
print('Success rate: ${metrics.successRatePercentage}%');
// Check connectivity
final isConnected = OfflineSyncLayer.instance.connectivityMonitor.isConnected;
print('Connected: $isConnected');
// Check initialization
final isInitialized = OfflineSyncLayer.instance.isInitialized;
print('Initialized: $isInitialized');
Troubleshooting #
โ HiveError: A Hive box with name 'offline_queue' already exists #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
โ Hive has not been initialized #
await Hive.initFlutter();
await OfflineSyncLayer.instance.initialize();
โ No Firebase App '[DEFAULT]' has been created #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // Initialize first
await OfflineSyncLayer.instance.initialize();
runApp(const ProviderScope(child: MyApp()));
}
โ Firestore persistence enabled too late #
await Firebase.initializeApp();
await FirebaseFirestore.instance.settings =
const Settings(persistenceEnabled: true); // Before sync layer
await OfflineSyncLayer.instance.initialize();
โ Queue Stuck Processing #
// 1. Force sync
await OfflineSyncLayer.instance.sync();
// 2. Verify handler is registered
OfflineSyncLayer.instance.registerOperationHandler('your_category', ...);
// 3. Check WiFi-only mode
if (syncConfig.syncOnWiFiOnly) {
final isWifi = await OfflineSyncLayer.instance.connectivityMonitor.isWifiConnected;
if (!isWifi) print('WiFi-only mode active');
}
// 4. Retry manually
await OfflineSyncLayer.instance.retryFailedOperation('item_id');
โ Duplicate Operations #
await OfflineSyncLayer.instance.submitOperation(
category: 'orders',
priority: 1,
data: orderData,
idempotencyKey: 'unique_order_${orderData['id']}', // Custom key
);
โ OfflineSyncLayer not initialized #
if (OfflineSyncLayer.instance.isInitialized) {
await OfflineSyncLayer.instance.submitOperation(...);
} else {
print('Sync layer not ready yet');
}
Debug Checklist #
- โ
OfflineSyncLayer.instance.isInitializedistrue - โ Handlers registered for all categories
- โ Firebase initialized before sync layer
- โ Firestore persistence enabled before sync layer
- โ Connectivity monitor working
- โ Queue has items
- โ No console errors
- โ Hive initialized
- โ Unique idempotency keys used
- โ WiFi-only mode not blocking (if enabled)
FAQ #
Q: Does it work offline? A: Yes. Queue operations persist locally using Hive and sync automatically when online.
Q: What happens if the app restarts? A: Queue data persists using Hive and resumes automatically.
Q: Does it support Firebase? A: Yes. Full Firestore, Storage, and Auth integrations are included.
Q: Can I use custom backends? A: Yes. Works with REST APIs, GraphQL, 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.
Q: Do I need Firestore persistence enabled?
A: Recommended for a complete offline-first experience. It works alongside riverpod_offline_sync.
Q: How does this compare to Firebase's offline?
A: Firebase handles query caching; riverpod_offline_sync handles operation queuing with retries, conflict resolution, and full control.
Q: Can I use both together?
A: Yes! Use Firestore native offline for reads and riverpod_offline_sync for critical writes needing control.
๐ฏ Handler Registration Checklist #
Required handlers to register in main():
- โ
firestore_setโ For create/set operations - โ
firestore_updateโ For update operations - โ
firestore_deleteโ For delete operations - โ
firestore_batchโ For batch writes (optional) - โ
api_requestโ For custom API endpoints - โ
analyticsโ For analytics tracking
No handler needed for:
- โ
Firebase Storage uploads (uses
StorageQueue) - โ Firestore real-time listeners (native)
๐ Package Comparison #
| Feature | Firebase Only | With riverpod_offline_sync |
|---|---|---|
| Offline reads | โ Yes (cached queries) | โ Yes (via Firestore) |
| Offline writes | โ Basic queue | โ Full queue control |
| Pause/Resume operations | โ No | โ Yes |
| Conflict resolution | โ Last write wins | โ Multiple strategies |
| Retry logic | โ Basic | โ Customizable |
| Queue inspection | โ No | โ Debug panel + metrics |
| Idempotency | โ Manual | โ Built-in |
| Progress tracking | โ No | โ Real-time streams |
| Upload control | โ No | โ Pause/Resume/Cancel |
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, and media upload apps.
Perfect for Firebase offline-first development! ๐