local_first_periodic_strategy
A reusable periodic synchronization strategy plugin for the LocalFirst framework. This plugin extracts the technical periodic sync mechanism into a reusable component, separating sync orchestration from business logic.
Note: This is a companion package to
local_first. You need to install the core package first.
Why local_first_periodic_strategy?
- Separation of concerns: Technical sync logic separated from business-specific API calls
- Reusable: Works with any REST API, backend, or database
- Callback-based: Inject your own business logic through simple callbacks
- Efficient: Uses server sequences for incremental sync (only fetch new events)
- Testable: Easy to mock callbacks and test independently
- Production-ready: Includes error handling, connection health checks, and retry logic
Features
- ✅ Periodic Timer Management: Configurable sync intervals
- ✅ Push-then-Pull Pattern: Orchestrates bidirectional sync automatically
- ✅ Connection State Reporting: Tracks and reports sync status to UI
- ✅ Event Batching: Efficiently batches multiple events in a single request
- ✅ Health Checks: Optional ping callback for connection monitoring
- ✅ Error Recovery: Continues syncing other repositories even if one fails
Installation
Add the dependency to your pubspec.yaml:
dependencies:
local_first: ^0.6.0
local_first_periodic_strategy: ^1.0.0
# Choose your storage adapter
local_first_hive_storage: ^0.2.0 # or
local_first_sqlite_storage: ^0.2.0
Then install it with:
flutter pub get
Quick Start
import 'package:local_first/local_first.dart';
import 'package:local_first_periodic_strategy/local_first_periodic_strategy.dart';
// 1) Implement your API client
class MyApiClient {
Future<List<JsonMap>> fetchEvents(String repo, {int? afterSeq}) async {
final response = await http.get(
Uri.parse('https://api.example.com/events/$repo?seq=$afterSeq'),
);
return (jsonDecode(response.body)['events'] as List).cast<JsonMap>();
}
Future<bool> pushEvents(String repo, LocalFirstEvents events) async {
final response = await http.post(
Uri.parse('https://api.example.com/events/$repo/batch'),
body: jsonEncode({'events': events.toJson()}),
);
return response.statusCode == 200;
}
}
// 2) Create sync state manager (tracks last sequence)
class SyncStateManager {
final LocalFirstClient client;
SyncStateManager(this.client);
Future<int?> getLastSequence(String repo) async {
final value = await client.getConfigValue('__last_seq__$repo');
return value != null ? int.tryParse(value) : null;
}
Future<void> saveLastSequence(String repo, int sequence) async {
await client.setConfigValue('__last_seq__$repo', sequence.toString());
}
int? extractMaxSequence(List<JsonMap<dynamic>> events) {
if (events.isEmpty) return null;
return events.map((e) => e['serverSequence'] as int?).whereType<int>().reduce((a, b) => a > b ? a : b);
}
}
// 3) Wire up the periodic sync strategy
final apiClient = MyApiClient();
final syncManager = SyncStateManager(client);
final strategy = PeriodicSyncStrategy(
syncInterval: Duration(seconds: 5),
repositoryNames: ['user', 'todo'],
// Fetch events from your API
onFetchEvents: (repositoryName) async {
final lastSeq = await syncManager.getLastSequence(repositoryName);
return await apiClient.fetchEvents(repositoryName, afterSeq: lastSeq);
},
// Push events to your API
onPushEvents: (repositoryName, events) async {
return await apiClient.pushEvents(repositoryName, events);
},
// Build sync filter (for your own tracking)
onBuildSyncFilter: (repositoryName) async {
final lastSeq = await syncManager.getLastSequence(repositoryName);
return lastSeq != null ? {'seq': lastSeq} : null;
},
// Save sync state after successful sync
onSaveSyncState: (repositoryName, events) async {
final maxSeq = syncManager.extractMaxSequence(events);
if (maxSeq != null) {
await syncManager.saveLastSequence(repositoryName, maxSeq);
}
},
// Optional: health check
onPing: () async {
final response = await http.get(Uri.parse('https://api.example.com/health'));
return response.statusCode == 200;
},
);
// 4) Initialize client with strategy
final client = LocalFirstClient(
repositories: [userRepository, todoRepository],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [strategy],
);
await client.initialize();
await strategy.start(); // Start syncing!
How It Works
Sync Flow
Every syncInterval seconds, the strategy automatically:
- Push Phase: Gets pending local events and pushes them via
onPushEvents - Pull Phase: Fetches remote events via
onFetchEventsand applies them locally - State Update: Saves sync state via
onSaveSyncStatefor incremental sync
┌─────────────────────────────────────────┐
│ PeriodicSyncStrategy (Plugin) │
│ ┌─────────────────────────────────┐ │
│ │ Timer (every 5 seconds) │ │
│ └─────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────▼───────────────────┐ │
│ │ Push pending events │ │
│ │ (calls onPushEvents callback) │ │
│ └─────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────▼───────────────────┐ │
│ │ Pull remote events │ │
│ │ (calls onFetchEvents callback) │ │
│ └─────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────▼───────────────────┐ │
│ │ Save sync state │ │
│ │ (calls onSaveSyncState) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
│ │
▼ ▼
Your API Client Your State Manager
Architecture
Technical Implementation (Plugin)
The plugin provides:
- Periodic timer management
- Sync orchestration (push → pull)
- Connection state reporting
- Event batching coordination
- Error handling and logging
Business Logic (Your App)
You provide through callbacks:
- API endpoint calls
- Authentication headers
- Repository-specific filtering
- Sync state management
- Event transformation
This separation makes the plugin reusable across any backend.
Comparison with WebSocketSyncStrategy
| Feature | PeriodicSyncStrategy | WebSocketSyncStrategy |
|---|---|---|
| Sync Timing | Periodic intervals | Real-time on event |
| Push Pattern | Batched | Immediate |
| Connection | Stateless HTTP/REST | Stateful WebSocket |
| Best For | REST APIs, polling | Real-time collaboration |
| Complexity | Simple | Moderate |
| Battery Usage | Low (configurable) | Higher (always on) |
Configuration Options
Sync Interval
Adjust the sync frequency based on your needs:
PeriodicSyncStrategy(
syncInterval: Duration(seconds: 10), // Every 10 seconds
// ... other params
)
Recommendations:
- 📱 Mobile apps: 5-10 seconds (balance between freshness and battery)
- 💻 Desktop apps: 2-5 seconds (users expect faster updates)
- 🌐 Web apps: 3-5 seconds (moderate polling)
Repository Names
Specify which repositories to sync:
repositoryNames: ['user', 'todo', 'settings'],
The strategy will sync all listed repositories in each cycle.
Advanced Usage
Custom Error Handling
The plugin logs errors but continues with other repositories. You can wrap callbacks for custom error handling:
onFetchEvents: (repositoryName) async {
try {
return await apiClient.fetchEvents(repositoryName);
} catch (e) {
// Custom error handling
errorTracker.log('Fetch failed for $repositoryName: $e');
return []; // Return empty list to continue
}
},
Conditional Syncing
Skip syncing for certain repositories based on conditions:
onFetchEvents: (repositoryName) async {
// Skip syncing if user is in offline mode
if (offlineMode && repositoryName != 'essential') {
return [];
}
return await apiClient.fetchEvents(repositoryName);
},
Multiple Strategies
Run multiple strategies simultaneously:
final client = LocalFirstClient(
repositories: [userRepo, todoRepo, messageRepo],
localStorage: HiveLocalFirstStorage(),
syncStrategies: [
// Critical data: frequent sync
PeriodicSyncStrategy(
syncInterval: Duration(seconds: 5),
repositoryNames: ['user', 'message'],
// ... callbacks
),
// Non-critical: less frequent
PeriodicSyncStrategy(
syncInterval: Duration(minutes: 1),
repositoryNames: ['settings', 'cache'],
// ... callbacks
),
],
);
Examples
Full working examples available in the monorepo:
- Hive + HTTP:
local_first_hive_storage/example- Uses REST API with periodic sync - SQLite + HTTP: Coming soon
Best Practices
- Use Server Sequences: Track
serverSequenceinstead of timestamps for reliable incremental sync - Batch Events: The plugin already batches - don't send events one by one in your API
- Handle Errors Gracefully: Return empty arrays or false in callbacks to continue syncing
- Test Offline: Verify your app works when
onPushEventsandonFetchEventsfail - Monitor Performance: Log sync times to identify slow repositories
Troubleshooting
Sync Not Working
- Verify the server is responding: check
onPingcallback - Enable verbose logging to see sync activity
- Check that
onSaveSyncStateis actually saving the sequence
Duplicate Events
- Ensure your API returns events based on
serverSequencefiltering - Verify
onSaveSyncStateis called after events are applied
High Battery Usage
- Increase
syncIntervalto reduce frequency - Use connection state monitoring to pause syncing when app is backgrounded
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
Support the Project 💰
Your contributions help us enhance and maintain our plugins. Donations are used to procure devices and equipment for testing compatibility across platforms and versions.
License
This project is available under the MIT License. See LICENSE for details.
Libraries
- local_first_periodic_strategy
- A reusable periodic synchronization strategy plugin for LocalFirst framework.
- periodic_sync_strategy

