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.
A production offline-first app has three distinct layers working together:
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.
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();
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 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.
The sync engine has four core responsibilities:
connectivity_plus
packageclass 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
}
}
}
}
When multiple devices edit the same record offline, conflicts occur. You need a strategy:
updatedAt
timestamp wins. Simple, works for 80% of apps.Don't leave offline testing to chance. Write automated tests for every user flow:
FakeConnectivity
class to simulate offline/online transitions in unit testsOur Flutter team builds production-grade mobile apps for iOS and Android — with offline-first architecture built in from day one.
Discuss Your Mobile App →

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