Files
doormile_milderapp/ABOUT_MILER.md
2026-08-11 13:16:33 +05:30

33 KiB
Raw Blame History

Miler — the Doormile rider app

Miler Rider — Smarter Pickups, Powered by AI Flutter · Android + iOS · com.doormile.partner · v1.2.28+144

This is the single reference for what this app is, who it serves, how it is built, and how it was designed. If you are new to the codebase, read sections 1–3. If you are about to change UI, read sections 5–7 before you write a line.


Contents

  1. What Miler is
  2. The people and the workflow
  3. The routing problem underneath
  4. Architecture
  5. Design principles
  6. The design system
  7. Screen by screen — what was designed and why
  8. Backend integration and known gaps
  9. What we are doing with this app now
  10. Conventions for anyone touching this code

1. What Miler is

Miler is the rider app built by Doormile, a logistics company that moves parcels between customers and its hubs. It is the tool carried by the field agent we call a "miler" — the person on a bike who actually collects and delivers packages on the ground.

The system works like this. Customers use the separate Doormile customer app to book a time slot — a window in which they want a parcel picked up or delivered. Behind the scenes, for each slot, a hub manager (admin) gathers all the customer bookings falling in that window and assigns them to one miler as a single ordered route — Stop 1, Stop 2, Stop 3, through to the last stop. That route appears inside this app in the exact sequence the admin set.

The miler goes On Duty, works the stops one by one in order, and at each stop does either a pickup or a delivery or both :

  • Pickup — collects the parcel from the customer or store, photographs it, takes payment if money is owed (cash or UPI/QR), confirms it as picked up.
  • Delivery — hands over a parcel carried from the hub, verifies with an OTP and a photo, confirms it as delivered. Deliveries are prepaid, so no money is collected.

He can skip a stop when a store is closed or a customer isn't available, and come back to it later. He cannot reorder the route — the sequence is fixed by the admin. After the last stop he returns to the same hub he started from, bringing back everything he collected plus anything he couldn't deliver.

In short: a first-mile + last-mile round trip, Hub → stop 1 → stop 2 → … → stop N → back to the same Hub, much like an Amazon or Flipkart delivery associate runs a route. This app is the miler's companion for the whole journey.

Doormile's two apps

App Who uses it What they do
Doormile customer app End customers Book a time slot for a pickup or delivery
Miler app (this one) The rider Carry out the assigned route

Who this app is for

One person, on a bike, working through an assigned list of stops for a shift. That is the entire user base. Every design decision in this document traces back to that single user and one fact about him: the daily loop repeats 30–50 times a day.


2. The people and the workflow

Role What they do Which app
Customer (CX) Books a time slot for a pickup or delivery Doormile customer app
Hub manager / Admin Bundles a slot's bookings into an ordered route and assigns it to a miler Admin / hub system
Miler (rider) Runs the route — pickups and deliveries — and returns to the hub This app

The loop, step by step

  1. Slot booking — customers book time slots in the customer app.
  2. Route assignment — for a slot, the hub admin groups the bookings and assigns them to one miler as a fixed-order route (Stop 1 … Stop N).
  3. Route appears — the ordered stops show up in this app.
  4. On Duty — the miler goes online to start working.
  5. Work each stop in order
    • Pickup stop → navigate → collect parcel → photo proof (+ weight) → take payment if owed (Cash / UPI-QR) → confirm Picked up.
    • Delivery stop → navigate → hand over parcel → verify OTP + photo → confirm Delivered (prepaid, no payment).
  6. Skip if needed — a stop can be skipped and resumed later. The order of the remaining stops never changes.
  7. Move to next stop — after each success the app prompts the next stop.
  8. Return to hub — after the last stop, back to the same hub with all collected parcels and any undelivered (RTO) items.

Hard rules

  • One route per slot, assigned by the admin.
  • Stops are strictly ordered — the miler follows the sequence and cannot reorder it.
  • Skip is allowed and normal — not a failure; the stop can be resumed.
  • Round trip — always starts and ends at the same hub.
  • Pickup vs Delivery — each stop is one or the other; the app shows the type clearly and runs the right flow.

Vocabulary

Rider-facing language is deliberately narrow. Route (the whole assignment) → Stops (ordered, each tagged Pickup or Delivery) → Hub (start and return point). Avoid "booking", "task", and "order" in UI copy — they were used interchangeably in the old build and meant nothing specific to a rider.


3. The routing problem underneath

A miler's day is one solved Vehicle Routing Problem instance. The hub admin solves it; the rider executes it. He is explicitly not allowed to re-optimise — he can't reorder stops — but he absolutely needs to see the shape of the solution he has been handed, because every classic VRP constraint has a physical consequence on the bike.

This is the intellectual backbone of the Home screen redesign. Each constraint maps to something concrete:

VRP concept What it means to a miler Where it appears
Depot / round trip The route starts and ends at the same hub, so the return leg is real distance he has to ride hub → progress → hub rail + ≈ km round trip
Capacity (CVRP) Total parcels and weight must fit in the box. Discovering an overload at stop 9 is too late Parcels · weight kg
Time windows (VRPTW) Each booking has a customer slot. Arriving after it closes is a failed stop "N stops past their booked slot" / "close within 30 min"
Route duration limit The shift is the hard cap on the route. Time left ÷ stops left is the pace he is actually racing Shift countdown + "only X per stop left"
Fixed sequence The admin's order is the solution; re-sorting it would break it The list never reorders; skip-not-reorder
Service time Each stop takes real minutes (photo, weight, payment, OTP) Folded into the per-stop budget
Cash on route Not a textbook VRP term, but he is carrying it and is accountable for it ₹ to collect

All of this is derived from data the app already has — stop coordinates, quantities, expected_pickup_time, the shift window in prefs. No new endpoint was required. The computation lives in lib/views/Dashboard/home/route_brief.dart as a pure, unit-tested RouteBrief value object.

Two honesty constraints in that code:

  • Distance is straight-line haversine, so it under-reads real road distance. It is always rendered with a ≈ and never presented as a promise.
  • Accepted stops have left the Home list for the Bookings tab, but they are still on the route — their parcels still need collecting, their kilometres still need riding. RouteBrief.from(extraStops: …) pulls them back into the totals so the brief never understates the day.

Reference reading: FarEye — Vehicle Routing Problem, ArcGIS Pro — Solve Vehicle Routing Problem.


4. Architecture

Stack

Concern Choice
Framework Flutter (Dart SDK ^3.9.2)
State / DI / routing GetX (GetMaterialApp, Get.put, Get.offAll) + a little provider
Responsive sizing flutter_screenutil, design size 390 × 844 — .w .h .sp .r everywhere
Persistence shared_preferences (session, duty state, shift window, last GPS fix, accepted stops)
Maps & nav google_maps_flutter, flutter_polyline_points, geolocator, geocoding
Push Firebase Core + Messaging + flutter_local_notifications
Live tracking MQTT (mqtt_client) + flutter_foreground_task
Media image_picker (proof photos), minio (uploads), qr_flutter (UPI QR)
Motion lottie, shimmer, custom primitives in app_widgets.dart

Directory map

lib/
├── main.dart                  App bootstrap, Firebase, GetMaterialApp, lifecycle
├── helpers/
│   ├── app_bootstrap.dart     Decides the opening screen (replaced the splash)
│   ├── shift_end_alarm.dart   Android AlarmManager via MethodChannel doormile/shift_end
│   └── http_overrides.dart
├── data/
│   ├── api_config.dart        THE backend switch + legacy/v1 adapter
│   ├── accepted_store.dart    Locally persisted accepted stops
│   ├── assignment_lookup.dart bookingid → bookingassignmentid resolver
│   └── mock_bookings.dart     Demo data (gated behind !useNewApi)
├── Models/
│   └── stop_status.dart       Canonical StopStatus enum + raw-string normaliser
├── controllers/               GetX controllers (auth, duty, pickups, summary, …)
├── providers/                 HTTP layer, one per domain
├── background/                Foreground service, live tracking, background logs
├── utils/                     Kalman filter, MQTT service, device info
└── views/
    ├── introscreens/          First-run intro
    ├── onboardscreens/        Sign in, OTP, MPIN
    ├── Dashboard/
    │   ├── home/              Home + route_brief.dart
    │   ├── pickups/           The whole per-stop flow (see §7)
    │   ├── summary/           Earnings
    │   └── profile/           Account, support, rewards, FAQ
    ├── helpers/constants/     Colors, fonts, spacing, theme
    ├── helpers/widgets/       Shared kit: app_widgets.dart, miler_app_bar.dart
    └── offline/               Full-screen offline state

App entry

main.dart initialises Firebase, registers permanent controllers (ProfileController, RiderLogController, PickupController, LogController), starts logging, checks whether the shift already ended while the app was closed, and runs GetMaterialApp with AppBootstrap as home.

AppBootstrap (lib/helpers/app_bootstrap.dart) decides the opening screen and nothing else:

  • app version changed since last run → force re-verification (MPIN if the rider was signed in, otherwise Sign In)
  • signed in + on duty → dashboard
  • signed in + off duty → the go-on-duty banner
  • signed out, intro already seen → Sign In
  • first ever launch → Introscreen

This replaced a four-second animated splash screen. That splash held two jobs: a brand animation and these routing rules. Only the rules were load-bearing. The animation was pure cost for someone opening this app 30–50 times a shift, layered on top of the native launch image that already covers cold start. The routing moved across intact; the four seconds are gone.

Shell

BottomPage (lib/widget/Bottom_page.dart) is a floating pill nav over an IndexedStack — four tabs, state preserved, no page ever mounted twice:

Tab Screen Purpose
Home Homepage Greeting, duty toggle, route brief, new stops
Bookings MyPickups The active route — ordered stops, hub → progress → hub
Earnings Summary Pickup counts, success rate, reward points, distance
Account ProfilePage Rider details, support, settings

Tab switches run a single 280 ms fade + rise controller. Pushed pages use Transition.cupertino at 300 ms globally.

Background and location

Location is the app's most safety-critical data path, so it gets real treatment:

  • MilerKalmanFilter (lib/utils/kalman_filter.dart) — a 4-D Kalman filter ([lat, lng, v_lat, v_lng]) smoothing raw GPS. Phone GPS on a moving bike is noisy; unsmoothed fixes produce jumping breadcrumbs and inflated distances, which matters because distance feeds rider payout.
  • LiveTrackingService — position stream → Kalman → MQTT publish, with battery level attached.
  • foreground_service.dart — keeps logging alive with the screen off via flutter_foreground_task in its own isolate.
  • ShiftEndAlarm — Android AlarmManager through a doormile/shift_end MethodChannel, so the shift-end break log fires even if the app is killed. This is why "Shift not set" is treated as a genuine fault, not a cosmetic gap: with no end time there is no alarm.

Duty state

DutyController is the single source of truth for "is the rider online?". Duty state used to be re-derived from raw prefs in a dozen places, and the server onduty int is clobbered by a flaky rider-log sync — which auto-kicked riders offline after a pickup. The controller fixes the priority in one place:

  1. onduty == 1 → a fresh server shift is authoritative
  2. explicit online flag → the rider's own toggle, which survives the flaky sync
  3. onduty == 0 → offline
  4. otherwise → default online

Similarly, StopStatus (lib/Models/stop_status.dart) normalises the backend's free-form orderstatus string. It used to be compared as raw literals in ~90 places with case-sensitivity landmines ('active' vs 'ACTIVE') and variant spellings ('picked' / 'picked up' / 'pickuped'). One enum, one parser, and every filter in the app now agrees.


5. Design principles

These are not aspirational. Each one was derived from a specific failure in the old build and is enforced in the code today.

1. The 30–50× rule

The daily loop repeats 30–50 times a day. Any confusion or extra tap becomes real frustration multiplied many times over. This is the lens for every trade-off: a one-second delay is not one second, it is a minute a day; a redundant tap is not one tap, it is fifty.

Consequence: the splash animation was deleted. Slide gestures were replaced with taps everywhere except money.

2. One screen, one primary action

The rider is standing at a gate with a parcel under one arm. There is exactly one thing he wants to do next. That thing gets the biggest, highest-contrast control; everything else recedes.

Consequence: the confirm sheet's hidden "swipe to update status" became three visible tap buttons — one primary (Picked up / Delivered), two secondary (Skip, Cancel). The Home stop card was cut down to one identity and one primary action.

3. Gestures are earned, not default

A slide is a deliberate friction device. It belongs only where an accidental tap would be expensive. Everything else is a tap.

Consequence: slides remain only on payment confirmation and going Off Duty. Start-pickup, arrived, and status confirm are all taps now.

4. Alert only when actionable; stay silent when healthy

If a coloured banner appears when nothing is wrong, riders learn to ignore all coloured banners. Warnings must be rare enough to still mean something.

Consequence: the route brief's risk lines render only when there is risk. A healthy route is a short, calm card. "Shift set" is a quiet grey strip; "Shift not set" is a full warning surface with a pulsing icon, the consequence spelled out, and a Retry — because an alert with no way out is just noise.

5. Never move things under the finger

The Bookings list polls every few seconds. Re-sorting a list while a thumb is descending on it is how riders tap the wrong stop.

Consequence: polls don't reorder the visible list mid-interaction. Reveal animations are keyed per logical item and play once, in initState, so a 3-second poll never replays them.

6. Readable at arm's length, in sunlight, with gloves

Minimum font sizes were raised (the old build went down to 7sp). Touch targets have a 44 px floor. Contrast is checked against a white surface in daylight, not against a designer's dark monitor.

7. Colour carries meaning, never decoration

See §6. Green means go. Maroon means brand or pickup. Blue means delivery. Amber means warning or skip. Red means danger. A colour never appears for variety.

8. Motion is feedback, not garnish

Every animation answers a question: did my tap register (PressScale), is this new (Reveal), is something loading (shimmer skeletons), did I change tabs (the nav fade + rise). Nothing animates just to look expensive.

9. Degrade gracefully, log loudly

The backend has real gaps (§8). The app never blocks on a missing endpoint — it falls back, logs [API_GAP], and keeps the rider moving. A rider stuck at a gate because a summary endpoint is missing is an unacceptable failure mode.

10. Honest numbers

If a number is an estimate, it is rendered as one. Straight-line distance shows ≈. Missing data renders —, never 0. A metric that can't include accepted stops doesn't get called "Stops left" until it does.


6. The design system

Reuse the tokens. Do not hardcode new shades or sizes.

Colour — lib/views/helpers/constants/Colorconstants.dart

Semantic roles (agreed; use these names, not raw hex):

Role Token Value
Brand / pickup accent ColorConstants.primary #960019 maroon
Go / success / positive completion ColorConstants.acceptGreen #12B76A
Delivery / info accent (in stop_type.dart) #2563EB blue
Danger / cancel ColorConstants.errorRed #DC2626
Warning / skip ColorConstants.warning #B45309 amber

The green was consolidated from four competing values (#16A34A, #34C759, #2E7D32, #4CAF50) that were scattered across Accept, Picked up, Delivered, Confirm, payment dialogs, verify success, and the done screen. They are now one token. A few #007AFF and #BA1A1A strays remain in nav and summary.

The full palette is a Material-3 role set — surface*, onSurface*, outline*, primary/onPrimary/primaryContainer, error*, warning*, success* — so new surfaces have a correct token without inventing one.

Typography — Font_constant.dart

Manrope, bundled as the OFL variable font (assets/fonts/Manrope/Manrope-VF.ttf). It is the free, legal stand-in for Uber Move, which is proprietary and cannot be shipped. Fallback is Plus Jakarta Sans.

Every text style in the app routes through FontConstants.fontFamily — there are no hardcoded family strings in lib/, so a single constant swaps the entire app's typeface. Scale runs displayLg 32 → headlineMd 24 → headlineSm 20 → bodyLg 18 → bodyMd 16 → labelBold 14 → labelSm 12.

Spacing, radius, shadow — design_constants.dart

4 px base scale (spacingXs 2 … spacing5xl 40), radius scale (radiusXs 2 … radius2xl 20, radiusFull 999), and five shadow levels (shadowXs … shadowXl).

Shared components — views/helpers/widgets/

Component File Use
AppCard app_widgets.dart White rounded card with system shadow
PrimaryButton app_widgets.dart Solid maroon CTA
Reveal app_widgets.dart One-shot fade + rise; keyed, plays once
staggerDelay(index) app_widgets.dart Staggered list entrance
PressScale app_widgets.dart Press feedback for custom tappables
SkeletonBone / MilerShimmer / skeletonCard() / SkeletonList app_widgets.dart Shimmer loading, replaces bare spinners
MilerAppBar miler_app_bar.dart The one top bar — logo, title, subtitle, trailing, 60 px, hairline
RouteBrief / RouteBriefCard / ShiftBanner home/route_brief.dart VRP route summary + shift alert
stopKindOf / StopKindUi pickups/stop_type.dart Pickup vs delivery branch — accent, icon, labels, verbs

Do not hand-roll an AppBar. Use MilerAppBar. Home is the one exception — it carries a richer dashboard header — and its logo and typography are kept aligned to the shared bar by hand.

Auth screen pattern

All onboarding screens follow one shape: top-anchored content (heading and input in the upper third, never vertically centred — this fixed the "input box too low" complaint), a large left-aligned heading (26sp, w700, -0.6 tracking), a one-line helper (14sp, height 1.45), and a pinned footer with a single high-contrast CTA — solid maroon fill, white text, w700, radius 14, 56.h, disabled at alpha .35. Structure: top Column → Expanded(SingleChildScrollView) → pinned footer. Mirror this for any new auth screen.


7. Screen by screen — what was designed and why

Home — views/Dashboard/home/homepage.dart

The rider's staging area: who he is, whether he's on duty, what the day looks like, and what's new.

Header. The brand mark sits at 56 px on a bordered, brand-tinted tile. At its old 46 px, flat on a white header, it read as a smudge rather than a logo — the tile gives it an edge. Next to it, "Hi, {name}" is the heaviest type in the header (17sp / w800, near-black), with Miler · {city} beneath at 11.5sp / w600 secondary. The greeting is the anchor of the screen, so it gets the weight; the location is context, so it recedes. On the right, the duty toggle.

Shift banner. Two deliberately different states. Set is a quiet grey strip with the window and time remaining — confirmation, not news. Not set is a warning surface with a left accent bar, a pulsing icon, the consequence stated plainly ("duty hours and the shift-end reminder are off"), and a Retry. Without a shift window the app cannot close duty and cannot fire the shift-end alarm, so this is an operational fault. Retry re-reads prefs and says so honestly when the fix is off-app — there is no standalone endpoint to re-pull the shift, only the login / MPIN profile sync.

Route brief. The VRP card described in §3 — a hub → progress → hub rail showing X of Y stops done, then stops left · parcels + weight · ≈ km round trip · ₹ to collect, then risk lines only when there is risk.

Stop cards. Redesigned Uber-style: PICKUP/DELIVERY badge + order id + circular select in the header, a hero name (16.5sp w700, -0.3 tracking), pin + address, a grey items chip, and labelled Call / Details buttons. Removed along the way: a misleading two-dot route timeline (a single stop is not a route — now a compact location badge), and a redundant second identity row.

Bookings / My Pickups — views/Dashboard/pickups/pickups.dart

The active route: the ordered list of stops with a hub → progress → hub overview. Staggered Reveal on entry, keyed per stop so the poll never replays it.

The per-stop flow — views/Dashboard/pickups/

File Role
card.dart The stop card in the route list
map.dart Map preview, navigation launch, and the owner of the success-screen transition
multi_map.dart Whole-route map with real step numbers
map_btn.dart The map-view entry row on the stop card
pip.dart Picture-in-picture info card while navigating
pickup_verify.dart Proof capture — photo, weight, OTP, signature plumbing
sheet.dart The confirm bottom sheet
skip_sheet.dart Skip with reason
payment_screen.dart Cash / UPI-QR collection
done.dart Success screen + "move to next stop"

Critical navigation architecture — do not regress this. The confirm sheet (_PickupBottomSheet._handleConfirm) must not navigate to PickupsDone itself. Pushing a full page from inside a modal bottom sheet is unreliable — the success screen silently never showed, and an isCurrent race then popped everything. Instead the sheet pops with an outcome map:

{'outcome': 'completed'|'cancelled', 'isDelivery': bool,
 'bonusPoints': int, 'paymentMethod': String}
// skip pops `true`; dismiss pops `null`

The caller — map.dart's _startPickupNavigation, which is a normal page route — inspects the result and does Navigator.pushReplacement(context, PickupsDone(...)) from its own route context (map.dart:788). It also removes the finished stop from the list first, so it doesn't linger as "active" blocking the next stop. This is what makes "Move to next stop" appear after picked up / delivered / cancelled.

Other flow decisions: no-payment pickups skip the payment-method list entirely (the reason list only shows for cancel or pickup-with-collection). The signature step was dropped for pickups — photo plus weight is enough proof. Test pre-fills in pickup_verify.dart were removed.

Earnings — views/Dashboard/summary/summary.dart

Pickup counts, success rate, reward points, distance ridden. The hero count is wrapped in an AnimatedSwitcher (fade + scale) so switching period animates rather than cutting.

Account — views/Dashboard/profile/

Rider details, support tickets, FAQ, notifications, alert sound, saved addresses, rewards, terms. Page-entrance Reveal; PressScale on the tile rows.

Offline — views/offline/offline_page.dart

A full-screen state with a breathing halo, stacked over the app whenever connectivity drops. Retry routes back through AppBootstrap, which re-decides login vs. home from scratch.


8. Backend integration and known gaps

The app targets the new v1 backend — https://api.doormile.com/api/v1, Bearer-token auth, {success, data, message} envelopes, camelCase — alongside the legacy jupiter/workolik backend.

Strategy: adapter + env flag. lib/data/api_config.dart is the one switch and mapper:

  • ApiConfig.useNewApi — defaults true; override with --dart-define=USE_NEW_API=false to go back to the old backend and mock data.
  • ApiConfig.toLegacyEnvelope() re-wraps v1 responses into the legacy {status, details} shape with legacy field names.
  • Providers branch on the flag and call the new endpoints; the ~90 UI files are untouched.

Two mock layers had to be disabled for live data: getMockQueues() / getMockBookingPageItems() fallbacks (now gated behind !useNewApi), and the auth controller, which used to fake a Demo Rider (userid 9999) and bypass the server entirely. In live mode verifyPinWithServer now does a real POST /miler/verify-pin and succeeds only on a server OK plus a stored token.

The pasted API doc was inaccurate (verified live 2026-07-18). verify-pin returns the login under user / user.profile, not data. The mapper reads the real shape. Booking field names are still based on the unreliable doc — re-verify the pickupFromBooking mapping once a real booking is assigned.

AssignmentLookup (lib/data/assignment_lookup.dart) exists because of one of these mismatches: POST /miler/assignments/{id}/accept keys on bookingassignmentid, but everything the app renders comes from GET /miler/bookings, whose rows only carry bookingid. Different sequences, different values. Sending a booking id made the backend answer 404 while the rider saw success and the booking was never actually accepted. GET /miler/assignments is the only place the pairing is exposed, so the app fetches it and keeps a short-lived map.

Known backend gaps

The app degrades gracefully around all of these and logs [API_GAP]:

  • no skip / resume endpoint
  • no booking-cancel endpoint
  • no per-stop type field (pickup vs delivery) and no step ordering
  • booking carries no assignmentid / consignmentid — accept, reject and deliver assume they equal the booking id
  • no COD amount on the booking
  • no update-PIN or rider-count endpoints
  • no bonus-summary endpoint (rewards are derived from /miler/earnings)

Release safety

Demo and test toggles are tied to kDebugMode so they auto-disable in release builds: kBypassGeofenceForTesting (pickups_controller.dart), kDemoMixedRoute (stop_type.dart), and the summary weekly-kms mock fallback.


9. What we are doing with this app now

The programme is one thing: make the app simple enough that a rider running it 30–50 times a day never has to think about it. The app was already well-built — branded app bars, a coherent maroon palette, a premium Earnings hero, clean cards. It did not need a layout rewrite. It needed clarity, motion, and honest data.

Done

  • Terminology and IA — one noun set (Route / Stops / Hub). Home = new stops and route start; Bookings = the active route.
  • Typeface — Plus Jakarta Sans → Manrope, verified on device.
  • Semantic colour consolidation — four greens → one acceptGreen; roles documented and applied.
  • Slide fatigue killed — start-pickup and arrived became taps; the confirm sheet's hidden swipe became three visible buttons. Slides remain only on payment and Off Duty.
  • Motion layer — Reveal, PressScale, shimmer skeletons; staggered lists on Bookings, Home and notifications; Transition.cupertino globally; animated tab switches over a state-preserving IndexedStack.
  • Auth screens — one top-anchored pattern with a pinned solid-maroon CTA; logo shrunk 180 → 104.h so content sits higher.
  • Mixed pickup/delivery plumbing — stop_type.dart gives one branch point; the delivery flow is fully wired end to end (card badge, OTP + photo verify, no-payment "Delivered", done screen) behind kDemoMixedRoute.
  • Duty and status normalisation — DutyController and StopStatus ended the "rider randomly goes offline" and "status string mismatch" classes of bug.
  • Splash screen deleted — routing preserved in AppBootstrap, four seconds per launch removed.
  • Home redesign — bigger, framed logo; bold greeting; shift-not-set promoted to a real alert with a Retry; and the VRP route brief (§3), covered by 15 unit tests in test/route_brief_test.dart.

Next

  1. Ship mixed routes for real. Everything on the app side is built. It is blocked on the backend sending a per-stop type (pickup | delivery) and a step order. The moment it does, stopKindOf prefers it automatically and kDemoMixedRoute becomes irrelevant.
  2. Re-verify pickupFromBooking against a real assigned booking (§8).
  3. Retire the accept/reject metaphor on Home. This is not an on-demand marketplace — the route is pre-assigned, so multi-select checkboxes plus "Select All" plus single-tap accept is three interaction models for a decision the rider doesn't actually get to make. Reframe as start assigned route.
  4. Make skip/resume first-class in the route view, and get a real backend endpoint behind it.
  5. Close the remaining colour strays (#007AFF, #BA1A1A) in nav and summary.
  6. Real road distance in the route brief once a routing/matrix API is available — the ≈ haversine is a deliberate placeholder.

10. Conventions for anyone touching this code

  1. Read §7's navigation warning before touching the pickup flow. The sheet-pops-an-outcome architecture is load-bearing and was arrived at after a silent-failure bug.
  2. Use the tokens. ColorConstants, DesignConstants, FontConstants. No new hex values, no hardcoded font families, no magic spacing.
  3. Use the shared components. MilerAppBar for headers, AppCard for cards, PrimaryButton for CTAs, Reveal/PressScale/skeletons for motion and loading. Hand-rolling one of these is how the app got four different greens.
  4. Size with ScreenUtil. .w .h .sp .r against the 390 × 844 design size. Use .w (not .sp) for anything that must not resize with the user's font setting — logos, icon frames.
  5. New gestures are taps. A slide needs a justification: it is only for actions that would be expensive to trigger by accident.
  6. Keep Reveal keyed. An unkeyed Reveal in a polled list replays every few seconds and looks broken.
  7. Never block the rider on a missing endpoint. Fall back, log [API_GAP], keep him moving.
  8. Render honesty. ≈ for estimates, — for missing, never a fake 0.
  9. Run flutter analyze lib/ test/ before you call it done. The target is zero errors; the existing infos are legacy noise, don't add to them.
  10. Test the pure logic. RouteBrief is a value object with no Flutter dependency precisely so it can be unit-tested. Follow that pattern for new derived data.

The core idea, simply put

A miler is handed a fixed, ordered route for a time slot, does every pickup and delivery on it in sequence, and brings everything back to the hub he started from — and this app guides him through every stop of that journey.

Everything else in this document is in service of making that loop cost him as little attention as possible, fifty times a day.