Blog Flutter · Mobile

Building Offline-First Flutter Apps:
A Production-Ready Architecture

👩‍💻Tharushi Senarath
· May 12, 2025 · 9 min read
Building offline-first Flutter apps — production-ready architecture with Isar, Hive, and sync engines

The assumption that users always have a reliable internet connection is one of the most dangerous assumptions in mobile development. Network coverage is inconsistent everywhere — users go through tunnels, lose signal in basements, travel to areas with poor connectivity. An app that crashes or freezes when offline earns 1-star reviews and uninstalls. Building offline-first from day one is far easier than retrofitting it later. Here's the architecture we use in production Flutter apps at Optwaves.

The Three-Layer Architecture

A production offline-first app has three distinct layers working together:

  • Local Data Layer — on-device database that is always the source of truth for reads
  • Sync Engine — the component that manages the two-way synchronisation between local and remote
  • Remote Data Layer — your REST API or GraphQL endpoint

The critical rule: all reads go to local storage, all writes go to local storage first. The sync engine handles propagating writes to remote when connectivity is available. The app never waits for a network response to show the user their data.

Choosing Your Local Database

Isar (Our Top Recommendation)

Isar is a pure-Dart embedded database — no native code, no platform setup headaches. It's benchmarked at roughly 100x faster than SQLite for typical mobile workloads. It supports full ACID transactions, schema migrations, and a powerful Dart-native query API. For most Flutter apps handling structured data, Isar is our default choice.

// Define a collection
@collection
class Task {
  Id id = Isar.autoIncrement;
  late String title;
  bool isCompleted = false;
  late DateTime updatedAt;
  bool isSynced = false; // track sync status
}

// Query locally (instant, no network)
final pendingTasks = await isar.tasks
  .filter()
  .isSyncedEqualTo(false)
  .sortByUpdatedAtDesc()
  .findAll();

Hive

A lightweight key-value store. Excellent for simple data like user settings, authentication tokens, and feature flags. Not suitable for complex relational data or advanced queries. We use Hive alongside Isar — Hive for app state, Isar for business data.

Drift (SQLite)

Drift is a type-safe SQLite wrapper with code generation. Best when your data has complex relational structures that benefit from SQL joins, or when your team is already comfortable with SQL. The Drift query API is more verbose than Isar's but gives you the full power of SQL.

Building the Sync Engine

The sync engine has four core responsibilities:

  • Operation Queue: stores pending create/update/delete operations as they happen offline
  • Connectivity Listener: monitors network state using the connectivity_plus package
  • Push Sync: when connection is restored, processes the queue and sends pending operations to the server
  • Pull Sync: fetches changes from the server since the last sync using a timestamp-based delta approach
class SyncEngine {
  final connectivity = Connectivity();

  void init() {
    connectivity.onConnectivityChanged.listen((result) {
      if (result != ConnectivityResult.none) {
        processPendingQueue(); // push local changes
        pullFromServer();      // fetch remote changes
      }
    });
  }

  Future<void> processPendingQueue() async {
    final pending = await isar.operations
      .filter().syncedEqualTo(false).findAll();
    for (final op in pending) {
      try {
        await api.applyOperation(op);
        await isar.writeTxn(() => isar.operations
          .put(op..synced = true));
      } catch (e) {
        // exponential backoff retry
      }
    }
  }
}

Handling Sync Conflicts

When multiple devices edit the same record offline, conflicts occur. You need a strategy:

  • Last-write-wins (LWW): The record with the most recent updatedAt timestamp wins. Simple, works for 80% of apps.
  • Server-wins: Remote data always overrides local. Use for read-heavy apps where users are unlikely to edit the same record simultaneously.
  • Field-level merge: Compare individual fields and merge non-conflicting changes. Complex to implement but provides the best user experience for collaborative features.

Testing Offline Behaviour

Don't leave offline testing to chance. Write automated tests for every user flow:

  • Create a FakeConnectivity class to simulate offline/online transitions in unit tests
  • Test that all CRUD operations complete and persist locally without network
  • Test that sync completes correctly after reconnection with no data loss
  • Test conflict resolution scenarios with crafted conflicting records
  • Test sync failure recovery with simulated API errors during sync

Need a Reliable Flutter Mobile App?

Our Flutter team builds production-grade mobile apps for iOS and Android — with offline-first architecture built in from day one.

Discuss Your Mobile App →

Related Articles

Ready to Build Something
Extraordinary?

The same expertise behind these articles goes into every project we build.