How the Flutter app is built, how it talks to the shared Supabase backend, its offline queue, and how it is released.
The mobile app is an Android app written in Dart 3.11 with Flutter 3.41, in its own repository (FT-AAFAT-MOB). Like the website, it has no server of its own: it talks to the shared Supabase backend directly, with the publishable key only. The website is the source of truth: the app ports the website's plain logic (hazard colours, record parsing, facets, badge progress, notification wording) line for line, and rebuilds the screens natively.
| Package | Used for |
|---|---|
provider |
State: one ChangeNotifier per area (records, auth, sync, badges, notifications, …). |
go_router 17 |
Navigation: four tabs (Map, Prepare, Data, Emergency) and full-screen routes. |
supabase_flutter 2 |
Reading and writing data, and signing in. |
flutter_map 8 |
The map, with Esri tiles in English and OpenStreetMap tiles in Urdu. |
connectivity_plus, shared_preferences |
The offline queue and its persistence. |
image_picker, flutter_image_compress, record, audioplayers |
Photos and voice memos on reports. |
geolocator, geocoding |
"Use my current location" and the "you are here" pin. |
workmanager, flutter_local_notifications |
Nearby hazard alerts in the background. |
pdf, share_plus, file_selector |
Record PDFs, sharing, and picking CSV files. |
flutter_svg, lucide_icons_flutter |
The website's badge medals, and icons. |
| Folder | What is in it |
|---|---|
lib/screens/ |
One folder per screen: map, prepare, emergency, data, contribute, auth, submissions, badges, notifications, admin, partner. |
lib/widgets/ |
Shared pieces: map layers and pins, filter sheets, the app bar, sheets. |
lib/providers/ |
State and the calls that fill it. |
lib/data/ |
Repositories (one per table), models and bundled data. |
lib/utils/ |
Pure logic, much of it ported from the website, each with tests. |
lib/services/ |
The offline queue, media, weather and notifications. |
lib/i18n/ |
The English and Urdu strings. |
assets/data/ |
The hazard guides and other content, bundled so they work offline. |
The app carries only the publishable key. Row-level security is the real boundary; the app adds no privileges of its own.
Signing in is email and password through Supabase Auth (PKCE). A password reset is requested through a second, implicit-flow client, so the emailed link works on any device and lands on the website's reset page. Sign-up by mobile number exists in the code but needs SMS, which the shared project does not have switched on.
| Reads | Writes |
|---|---|
map_records: approved rows only, without the media columns; one row's photo and voice note when opened; the user's own rows for My reports |
map_records: new rows, always pending (review fields left empty) |
official_reports (three scoped queries), official_relief, osm_facilities |
confirm_report(): "Yes, it's the same" on a duplicate |
reporter_attribution(): uploader and approver names, and badges |
user_notifications: read_at only |
my_badge(), user_notifications, profiles (the user's own; all of them for an administrator) |
profiles.role: administrators only, from User & role management |
The app never reviews: it makes no update to map_records, and it never calls the badge-review functions (badge_decide, admin_set_badge, badge_queue, badge_standings). A test scans the code to keep it that way. The weather forecast comes from Open-Meteo, not Supabase, and is never stored.
A report is never sent directly: it goes into a persisted outbox (SyncService), saved on the phone with its photo and voice memo embedded as compressed data. The outbox sends rows one at a time, as soon as the phone is online, on reconnect and when the app comes back to the foreground. A row leaves the outbox only once the server has accepted it. A row the server rejects (a constraint or permission error) is set aside and reported, so it cannot block the rest.
Duplicates are kept down in three ways:
looks_like hint for the reviewer;There is no server-side idempotency key yet. If the app is killed in the moment between the server accepting a row and the phone removing it from the outbox, that row is sent again on the next launch. A reviewer can link the two.
user_notifications every 60 seconds while the app is open. Nearby hazard alerts run in the background through Android WorkManager, about every 15 minutes, from the last location the app saved. There is no push server, so an alert can be late.There are no build flavours. The backend comes from a bundled assets/config/env.json (publishable key only), or from --dart-define=SUPABASE_URL=… --dart-define=SUPABASE_KEY=…, which wins when given, so a test build can point at a local copy. Debug and profile builds allow plain http for that local copy; release builds do not.
Releases are signed Android App Bundles (flutter build appbundle --release), with the version code raised for every upload. The signing key never enters a public repository.
flutter analyze must be clean, and flutter test runs about 840 tests: the ported logic against the website's own cases, the offline queue, roles and menus, the map layers, accessibility (contrast, tap targets, labels), both languages, and whole-app journeys. The app's own API calls have also been load tested with k6 against a local copy of the backend.