Surovi Agro Nexus+
An offline-first Flutter field-force management app for agricultural field officers — logging farmer visits, taking orders, and filing collection reports from villages where connectivity is a maybe, not a given.
Overview
Surovi Agro Nexus+ is an offline-first field-force management app built for agricultural field officers who spend most of their working day outside of signal range. It solves a specific, unglamorous problem: officers need to log farmer visits, take customer orders, and submit collection reports from villages where connectivity is a maybe — not a given — and none of that data can afford to be lost while they wait to sync.
That constraint shapes almost every engineering decision in the app. Every write happens to a local database first; the network is treated as an eventually-available upload channel, never a dependency for getting work done.
I built the app as a freelance engagement for Surovi Group, an agricultural conglomerate in Bangladesh whose field officers cover territory well beyond reliable network coverage.
The Problem
Surovi's field officers needed a single tool to run their day: visit farmers, capture leads, take orders, and file collection reports — all in areas where a mobile network signal is inconsistent at best. A web dashboard or an online-only app simply wasn't viable; officers couldn't be expected to lose a morning's work because a village had no coverage.
What started as lead capture grew over time into a full order-and-delivery workflow, then dark mode, then Bengali and English localization — each addition touching data models, local storage, network calls, and UI. That growth is exactly the point where architecture stops being a nice-to-have and starts determining whether the next feature takes two days or two weeks.
My Role
Freelance engagement for Surovi Group — architecture, implementation, and delivery:
- Designed the offline-first data layer: every write lands in SQLite first, with the network as a best-effort sync channel
- Architected the feature-first directory structure so leads, orders, collection reports, customers, and products could each ship as an isolated vertical slice
- Built the order-and-delivery workflow on top of the existing lead-capture foundation without touching unrelated features
- Implemented Bengali + English localization via Flutter's built-in
gen-l10n - Set up secure token storage with
flutter_secure_storageand connectivity-aware state via Riverpod
Key Features
Farmer Visit Logging Officers log farmer visits in the field, with all data captured locally first regardless of signal.
Order Taking
A multi-step order-creation flow, backed by a CurrentOrderNotifier that holds the in-progress draft until submission.
Collection Reports Field officers file collection reports from anywhere — written to local storage immediately, synced when connectivity returns.
Offline-First Sync Every create/update writes through the local database first. The app never blocks on network availability to let an officer keep working.
Dark Mode
A full dark theme, powered by design tokens centralised in core/theme/.
Bengali + English Localization
Full UI localization via .arb source files and Flutter's generated localization classes.
Technical Stack
| Layer | Choice |
|---|---|
| Language | Dart |
| Framework | Flutter (Material 3) |
| State management | Riverpod (NotifierProvider, StateProvider, ConsumerWidget) |
| Local persistence | SQLite (sqflite) + a JSON cache file for reference data |
| Network client | http, wrapped in a thin custom API client |
| Auth storage | flutter_secure_storage |
| Localization | Flutter's built-in gen-l10n (Bengali + English) |
Technical Decisions
Feature-First, Layered Within
Every business capability — leads, orders, collection reports, customers, products — is a self-contained vertical slice, and each slice is internally layered by responsibility:
Two rules do all the work here: group by feature, not by type — everything about orders lives under features/orders/, full stop. And layer within each feature by responsibility, not convenience — a screen never talks to SQLite or http directly, only through data_sources/. It's a lightweight take on Clean Architecture's data/domain/presentation split, sized for a Flutter app: models/ is the domain layer, data_sources/ is the data layer, screens/ + providers/ are presentation.
Offline-First Data Sources
Each feature gets a *LocalSource (raw SQLite CRUD) and a *RemoteSource (HTTP calls plus response parsing). This is the seam that makes offline-first tractable: OrdersRemoteSource.createOrder() writes through to OrdersLocalSource on success, so "did this order sync yet" is always answerable from one place, not scattered across UI state.
A Thin Services Facade
Api and AppDatabase in services/ are the only two classes most screens ever import for data access — they re-export each feature's data-source methods under one name. Screens don't need to know which feature a piece of data technically "belongs to" internally, and it's the single place to add cross-feature concerns like logging, retry policy, or auth-refresh without touching every feature.
Challenges & Solutions
Challenge: Every write needs to survive an officer going offline for hours between village visits, without duplicate orders or lost collection reports once they finally get signal.
Solution: data_sources/ is the only code allowed to touch persistence and network for a feature — remote writes go through the matching local source on success, so sync state is tracked in exactly one place rather than inferred from scattered UI state.
Challenge: The app grew from lead capture to a full order-and-delivery workflow, plus dark mode and localization, without slowing feature delivery down.
Solution: Feature-first folders mean adding a feature is adding a folder, not editing existing ones. When order-and-delivery tracking was added, it required zero changes inside leads/ or collection_reports/ — new data_sources/, models/, providers/, and screens/ subfolders under features/orders/, wired into services/ and core/router.dart, and the feature was live.
Challenge: Testing screens that touch both SQLite and HTTP meant either mocking the entire stack or testing nothing at all.
Solution: Because data_sources/ is the only layer touching real I/O, everything above it — providers/, screens/ — is tested against a fake *LocalSource/*RemoteSource without spinning up a database or a server.
Outcome
![]()
- Every write survives an officer going offline for hours between village visits — no duplicate orders, no lost collection reports, sync resolved automatically once connectivity returns
- The app grew from lead capture to a full order-and-delivery workflow, dark mode, and Bengali/English localization without a single architectural rewrite
- Feature isolation kept merge conflict risk near zero across the life of the project, and let each new capability ship as an isolated slice
- A new engineer who understands one feature's folder structure already understands every other feature's
- In production use by 100+ field officers across rural Bangladesh, on Google Play