Mobile app

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.

Main packages

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.

How the code is organised

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.

Talking to Supabase

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.

The offline queue

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:

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.

Maps, notifications and languages

Configuration and release

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.

Tests

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.

← All technical documentation