Building Flutter Apps That Work Offline

Building Flutter Apps That Work Offline

FlutterPulse

This article was translated specially for the channel FlutterPulseYou'll find lots of interesting things related to Flutter on this channel. Don't hesitate to subscribe!🚀

Many mobile apps are designed assuming the user will always be connected. But in the real world, network connectivity is often unreliable…

Many mobile apps are designed assuming the user will always be connected. But in the real world, network connectivity is often unreliable: airplanes, subways, rural areas, or just poor Wi-Fi. That's why offline-first design is crucial. In Flutter, implementing offline mode is straightforward if you plan for it in your architecture.

Why Offline Mode Matters

  • User experience → The app should not feel "broken" without internet.
  • Data integrity → Users should not lose data created offline.
  • Retention → Apps that gracefully handle offline usage are trusted more.

The Core Idea

Offline mode requires three things:

  1. Local storage for caching data.
  2. Sync strategy to reconcile local and remote data.
  3. Clean architecture that separates local and remote sources.

Architecture

A common pattern is the Repository pattern:

abstract class UserRepository {
Future<List<User>> getUsers();
Future<void> addUser(User user);
}

Implementation:

class UserRepositoryImpl implements UserRepository {
final RemoteDataSource remoteDataSource;
final LocalDataSource localDataSource;

UserRepositoryImpl(this.remoteDataSource, this.localDataSource);

@override
Future<List<User>> getUsers() async {
try {
final remoteUsers = await remoteDataSource.getUsers();
await localDataSource.cacheUsers(remoteUsers);
return remoteUsers;
} catch (e) {
// Fallback to cached data
return await localDataSource.getCachedUsers();
}
}
@override
Future<void> addUser(User user) async {
await localDataSource.addUser(user);
try {
await remoteDataSource.addUser(user);
} catch (_) {
// Mark as "pending sync"
await localDataSource.markPending(user);
}
}
}

Local Storage Options in Flutter

  1. SQLite (sqflite / drift) — structured data, queries, indexing.
  2. Hive — lightweight, fast, NoSQL-style storage.
  3. SharedPreferences — only for small key-value data.

Example with sqflite:

class LocalDataSource {
final Database db;

LocalDataSource(this.db);

Future<void> cacheUsers(List<User> users) async {
final batch = db.batch();
for (var user in users) {
batch.insert('users', user.toJson(),
conflictAlgorithm: ConflictAlgorithm.replace);
}
await batch.commit(noResult: true);
}
Future<List<User>> getCachedUsers() async {
final result = await db.query('users');
return result.map((e) => User.fromJson(e)).toList();
}
Future<void> addUser(User user) async {
await db.insert('users', user.toJson());
}
Future<void> markPending(User user) async {
await db.update('users', {'pending': 1},
where: 'id = ?', whereArgs: [user.id]);
}
}

Sync Strategy

When connectivity is restored:

  1. Check pending operations in local DB.
  2. Push them to the server.
  3. Mark them as synced once confirmed.

Example:

Future<void> syncPendingUsers() async {
final pendingUsers = await localDataSource.getPendingUsers();
for (var user in pendingUsers) {
try {
await remoteDataSource.addUser(user);
await localDataSource.markSynced(user.id);
} catch (_) {
// Keep as pending
}
}
}

Best Practices

  • Always read from cache first, then update with remote if available.
  • Keep a "pending" flag for unsynced operations.
  • Batch updates when syncing to reduce API calls.
  • Design API endpoints with sync in mind (idempotent, able to handle retries).

Conclusion

Offline mode is not just a feature — it's an expectation. Flutter makes it relatively simple with tools like sqflite, drift, and Hive. By combining a clear repository pattern, local caching, and sync logic, you can build apps that stay reliable regardless of connectivity.

Report Page