DEV Community

Jesso Joseph
Jesso Joseph

Posted on

How I built an offline-first Flutter app for forest rangers in zero-signal zones (USAID Forest-PLUS 3.0)

Imagine you're a forest ranger in the Western Ghats. No signal. No Wi-Fi. You need to log a wildlife sighting right now — and the app just shows a loading spinner.
That's the exact problem I was handed when building the Van App for the Kerala Forest Department under USAID's Forest-PLUS 3.0 program. This is how I solved it using Flutter and a proper offline-first architecture.

The problem

Forest rangers operate in deep forest zones where mobile connectivity is either non-existent or highly unreliable. They need to log forest survey entries, GPS coordinates, and incident reports in real time — but a standard API-first app would simply fail them.
The requirement was clear: the app must work fully without internet, and sync automatically the moment connectivity is restored. No data loss. No manual retry buttons. It just had to work.

*Offline-first vs. just caching — what's the difference?
*

Most apps treat offline as an edge case: they load data from the API, cache it, and show an error when the network drops. Offline-first flips this entirely.

Offline-first means: the local database is the source of truth. The app reads and writes to local storage by default. The network sync happens in the background, not in the foreground.

Three principles guided our architecture:

  1. Local-first data — every write goes to SQLite first
  2. A sync queue — pending writes are queued and replayed when online
  3. Conflict resolution — define a clear strategy before you write sync logic

Architecture overview

Our stack: Flutter + BLoC + Clean Architecture **+ **Drift (SQLite).
The data flow looks like this:

UI → BLoC → Repository → Local DB (Drift/SQLite)

Sync Queue → REST API (when online)

The Repository layer is the key piece — it decides whether to hit the local DB or the API based on connectivity state. The UI never knows the difference.

*The sync engine — the hard part
*

Detecting connectivity uses the connectivity_plus package:

final connectivity = Connectivity();
final result = await connectivity.checkConnectivity();

if (result == ConnectivityResult.none) {
await localDb.saveSurveyEntry(entry);
await syncQueue.enqueue(entry);
} else {
await apiService.submitSurveyEntry(entry);
}

Every pending item in the queue has a simple model:

class SyncQueueItem {
final String id;
final String endpoint;
final Map payload;
final DateTime createdAt;
int retryCount;

SyncQueueItem({
required this.id,
required this.endpoint,
required this.payload,
required this.createdAt,
this.retryCount = 0,
});
}

When connectivity is restored, a BLoC event triggers the sync replay:

class SyncBloc extends Bloc {
SyncBloc() : super(SyncInitial()) {
on(_onConnectivityRestored);
}

Future _onConnectivityRestored(
ConnectivityRestoredEvent event,
Emitter emit,
) async {
emit(SyncInProgress());
final pending = await syncQueue.getPending();
for (final item in pending) {
await _replayItem(item, emit);
}
emit(SyncComplete());
}
}

*Lessons learned
*

**1. Conflict resolution is harder than sync.
**When two rangers edit the same record while offline, you need a clear strategy before writing a single line of sync code. We used last-write-wins based on createdAt timestamp — simple, and it worked for our use case.
**2. Test on airplane mode from day one.
**We caught bugs in week 3 that should have been found in week 1. Make airplane mode testing part of your daily dev routine, not a pre-release check.
**3. The sync queue needs its own error states.
**A failed sync is not the same as a network error. Your UI needs to clearly show what's pending, what's synced, and what failed — users in the field need that visibility.
**4. Large payloads need chunked sync.
**If a ranger logs a high-res photo with a survey entry, syncing it as one payload can time out on a weak 2G edge connection. We split media uploads into a separate queue with smaller retry windows.

*Wrapping up
*

Building offline-first isn't just a technical challenge — it's a mindset shift. Once you stop treating the network as a requirement and start treating it as an optimization, the architecture becomes much cleaner.
The Van App is now used by Kerala Forest Department field officers as part of India's Forest-PLUS 3.0 carbon monitoring program. Knowing the app works reliably in zero-signal forest zones made the effort very much worth it.
If you're building something similar or have tackled offline sync differently, I'd love to hear your approach in the comments.

Top comments (0)