Offline-first apps are essential when users might not have a stable internet connection.
Offline-first apps are essential when users might not have a stable internet connection. Synchronizing local changes with a backend while handling conflicts can be tricky.
In this post, we’ll walk through an example Flutter app using Flutter SyncEngine, a package I built to simplify offline-first data syncing with conflict resolution.
By the end, you’ll have a notes app (example) that works offline and syncs reliably when online.
Supports multiple storage backends: File, Hive, and SQLite
Supports remote transport via REST (or a dummy transport for testing)
Tracks changes via SyncOperation
Handles conflicts using ConflictResolver (built-in LastWriteWins)
Fully offline-first: users can add notes while offline
The example app has the following structure:
lib/
├─ main.dart # Flutter app entry point
├─ stores/
│ ├─ file_sync_store.dart
│ ├─ hive_sync_store.dart
│ └─ sqflite_sync_store.dart
├─ transporters/
│ ├─ dummy_transport.dart
│ └─ rest_transport.dartmain.dart contains the full Flutter UI and integration with SyncEngine.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}We initialize Flutter and run our MyApp widget.
In _initEngine(), we:
Initialize the local storage based on the selected type (FileSyncStore, HiveSyncStore, SQLiteSyncStore)
Initialize the transport layer (RestTransport or DummyTransport)
Register the collection (notes) with a conflict resolver
engine = SyncEngine(store: store, transport: transport);
engine.registerCollection(
name: 'notes',
conflictResolver: const LastWriteWins(),
);This ensures any local changes are synced correctly with the backend.
Add a new note:
final id = DateTime.now().millisecondsSinceEpoch.toString();
final note = {
'id': id,
'title': 'Note $id',
'content': 'This is a new note',
'updatedAt': DateTime.now().toUtc().toIso8601String(),
};
await store.saveEntity('notes', note);
await store.logOperation(
SyncOperation(
collection: 'notes',
entityId: id,
type: OperationType.create,
timestamp: DateTime.now().toUtc(),
data: note,
),
);Sync with remote backend:
await engine.sync();
await _loadNotes();This pushes pending local changes and pulls remote changes while resolving conflicts.
The app supports switching between File, Hive, and SQLite stores at runtime:
void _changeStore(StoreType type) async {
setState(() {
selectedStore = type;
});
await _initEngine();
}This makes the app flexible for different storage needs.
The UI is a simple Flutter layout:
Top row: Buttons to switch between storage backends
List of notes
Floating buttons: Add new note & Sync
FloatingActionButton(
heroTag: 'add',
onPressed: syncing ? null : _addNote,
child: const Icon(Icons.add),
),
FloatingActionButton(
heroTag: 'sync',
onPressed: syncing ? null : _sync,
child: syncing
? const CircularProgressIndicator(color: Colors.white)
: const Icon(Icons.sync),
),Fully offline-first support
Conflict resolution is built-in and easy to extend
Pluggable storage and transport layers
Easy to integrate into any Flutter app
Conclusion
Flutter SyncEngine simplifies offline-first synchronization in Flutter apps. This example shows a fully functional notes app that works offline, tracks changes, syncs with a backend, and resolves conflicts automatically.
0
11
0