<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jesso Joseph</title>
    <description>The latest articles on Forem by Jesso Joseph (@jesso_joseph_0659a582c6d0).</description>
    <link>https://forem.com/jesso_joseph_0659a582c6d0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3853152%2F1cd7079a-269a-4d33-949f-41d93ec21ede.png</url>
      <title>Forem: Jesso Joseph</title>
      <link>https://forem.com/jesso_joseph_0659a582c6d0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jesso_joseph_0659a582c6d0"/>
    <language>en</language>
    <item>
      <title>How I built an offline-first Flutter app for forest rangers in zero-signal zones (USAID Forest-PLUS 3.0)</title>
      <dc:creator>Jesso Joseph</dc:creator>
      <pubDate>Tue, 31 Mar 2026 09:53:28 +0000</pubDate>
      <link>https://forem.com/jesso_joseph_0659a582c6d0/how-i-built-an-offline-first-flutter-app-for-forest-rangers-in-zero-signal-zones-usaid-forest-plus-1lp</link>
      <guid>https://forem.com/jesso_joseph_0659a582c6d0/how-i-built-an-offline-first-flutter-app-for-forest-rangers-in-zero-signal-zones-usaid-forest-plus-1lp</guid>
      <description>&lt;p&gt;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.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Offline-first vs. just caching — what's the difference?&lt;br&gt;
*&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline-first means&lt;/strong&gt;: 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.&lt;/p&gt;

&lt;p&gt;Three principles guided our architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Local-first data — every write goes to SQLite first&lt;/li&gt;
&lt;li&gt;A sync queue — pending writes are queued and replayed when online&lt;/li&gt;
&lt;li&gt;Conflict resolution — define a clear strategy before you write sync logic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Architecture overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our stack: &lt;strong&gt;Flutter&lt;/strong&gt; + &lt;strong&gt;BLoC&lt;/strong&gt; + &lt;strong&gt;Clean Architecture **+ **Drift (SQLite)&lt;/strong&gt;.&lt;br&gt;
The data flow looks like this:&lt;/p&gt;

&lt;p&gt;UI → BLoC → Repository → Local DB (Drift/SQLite)&lt;br&gt;
                              ↓&lt;br&gt;
                         Sync Queue → REST API (when online)&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;The sync engine — the hard part&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Detecting connectivity uses the connectivity_plus package:&lt;/p&gt;

&lt;p&gt;final connectivity = Connectivity();&lt;br&gt;
final result = await connectivity.checkConnectivity();&lt;/p&gt;

&lt;p&gt;if (result == ConnectivityResult.none) {&lt;br&gt;
  await localDb.saveSurveyEntry(entry);&lt;br&gt;
  await syncQueue.enqueue(entry);&lt;br&gt;
} else {&lt;br&gt;
  await apiService.submitSurveyEntry(entry);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Every pending item in the queue has a simple model:&lt;/p&gt;

&lt;p&gt;class SyncQueueItem {&lt;br&gt;
  final String id;&lt;br&gt;
  final String endpoint;&lt;br&gt;
  final Map payload;&lt;br&gt;
  final DateTime createdAt;&lt;br&gt;
  int retryCount;&lt;/p&gt;

&lt;p&gt;SyncQueueItem({&lt;br&gt;
    required this.id,&lt;br&gt;
    required this.endpoint,&lt;br&gt;
    required this.payload,&lt;br&gt;
    required this.createdAt,&lt;br&gt;
    this.retryCount = 0,&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;When connectivity is restored, a BLoC event triggers the sync replay:&lt;/p&gt;

&lt;p&gt;class SyncBloc extends Bloc {&lt;br&gt;
  SyncBloc() : super(SyncInitial()) {&lt;br&gt;
    on(_onConnectivityRestored);&lt;br&gt;
  }&lt;/p&gt;

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

&lt;p&gt;*&lt;em&gt;Lessons learned&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
**1. Conflict resolution is harder than sync.&lt;br&gt;
**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.&lt;br&gt;
**2. Test on airplane mode from day one.&lt;br&gt;
**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.&lt;br&gt;
**3. The sync queue needs its own error states.&lt;br&gt;
**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.&lt;br&gt;
**4. Large payloads need chunked sync.&lt;br&gt;
**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.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Wrapping up&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
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.&lt;br&gt;
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.&lt;br&gt;
If you're building something similar or have tackled offline sync differently, I'd love to hear your approach in the comments.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>mobile</category>
      <category>offline</category>
    </item>
  </channel>
</rss>
