Pravin Kunnure ✦

Feb 12, 2026 • 2 min read

Building a Flutter Offline-First Sync Engine with Conflict Resolution.

Offline-first apps are essential when users might not have a stable internet connection.

Introduction

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.

Key Features of the Example

  • 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

Project Structure

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.dart

main.dart contains the full Flutter UI and integration with SyncEngine.

Initializing the App

void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 runApp(const MyApp());
}

We initialize Flutter and run our MyApp widget.

Setting Up the Sync Engine

In _initEngine(), we:

  1. Initialize the local storage based on the selected type (FileSyncStore, HiveSyncStore, SQLiteSyncStore)

  2. Initialize the transport layer (RestTransport or DummyTransport)

  3. 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.

Managing Notes

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.

Switching Storage Backends

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.

User Interface

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),
),

Benefits

  • 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

Getting Started

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.

Join Pravin on Peerlist!

Join amazing folks like Pravin and thousands of other builders on Peerlist.

peerlist.io/

It’s available... this username is available! 😃

Claim your username before it's too late!

This username is already taken, you’re a little late.😐

0

11

0