← All case studies

Two-Sided Waste-Management Mobile App

Project Director · product scoping & delivery · 2025 – 2026

  • Flutter
  • Firebase
  • Figma
  • Realtime DB
  • Push Notifications

A pickup you could actually plan around

Commercial establishments (hotels, restaurants, residential complexes, offices) arranged waste collection by phone and habit. There was no schedule, no record of what got picked up, and no way to know whether a truck was ten minutes out or two hours late. The product is a two-sided marketplace that connects those waste generators with collection providers, which sounds like one build and is really two: a generator-side app for the customer who books a pickup, and a provider-side app for the dispatcher and driver who fulfil it. Both are Flutter on Firebase, and both have to agree on what a live pickup looks like at the exact same moment.

Phase one centred on the generator app, the demand side that proves the product, with the provider tooling and deeper analytics staged behind it. That choice is what kept a wide feature surface (scheduling, live tracking, history, profiles) from sinking a first release on a fixed timeline. But scoping to one side first only works if the two sides can be built and released on their own cadence, and that is a design problem before it is a scheduling one.

Two apps that only agree on one document

The seam between the apps is a single Firestore document. When a customer books, the generator app writes a request into a requests collection and it starts life as Pending. The provider side reads it, accepts it, assigns a driver, and walks the status through Accepted, Assigned, and Started. That status taxonomy is defined identically in both codebases, down to which states count as active and which count as history, so neither app can drift into a state the other does not understand.

Live tracking rides on a second document, live_location, and this is the real contract. Its model carries the driver’s current latitude and longitude, the pickup’s destination pin, the starting point, the driver’s name and number, plus a running ETA and distance string. The LiveLocationModel class exists byte-for-byte in both apps. The driver app is the only writer and the customer app is the only reader, so the document is the entire interface between the two products. Freeze that shape and each side can ship on its own schedule, which is exactly what let phase one lead with the customer app while the driver app followed. Nothing crosses the boundary except one well-known document, so there is no shared server code to keep in lockstep and no second API to version.

One grid, two calendars

Scheduling happens on a calendar, and in Nepal a calendar means two of them. The country runs on Bikram Sambat while most software underneath speaks Gregorian, so the booking screen renders both from a single grid. Every cell is a NepaliDateTime, and its Gregorian twin is just nepaliDate.toDateTime(). A toggle flips which numeral sits large in the centre of the day and which sits small in the corner, so the same grid reads as either calendar without rebuilding it.

The part that had to be right is where a booking lands. A pickup is stored against its Gregorian date, so when the calendar paints its dots it converts each Bikram Sambat cell back to Gregorian and matches on that, never on the label the user happens to be looking at. A day with active pickups shows up to three dots and then a plus, and it does so on the correct square whether the customer is thinking in Baisakh or in April.

When the truck starts moving

Once a driver starts a trip, the driver app subscribes to the device’s own GPS through onLocationChanged. Every time the phone reports a new fix, the app writes the fresh coordinates into the live_location document along with a recomputed ETA and distance. It is not a fixed heartbeat; the write follows real movement, so a truck sitting at a light does not spam the document and a truck on the move keeps it current.

Fig. 1 · One pickup across two apps
Customer app Firestore Driver app 1 · schedule pickup requests doc · status Pending 2 · provider accepts, assigns marked Accepted, then Started 3 · driver GPS streams on move current_lat / current_lon written 4 · customer app polls, 10s reads truck position + pin 5 · route drawn, truck moves on map driving route from truck to pin · ETA + distance

On the other side, the customer app polls that document every ten seconds. The truck’s dot comes straight from the coordinates, but the road it appears to drive does not. For that, the app asks the Directions API for a driving route between the driver’s live position and the customer’s pin, then paints the returned polyline. If that call comes back empty, on a rural road, past a quota, or a flaky moment, it falls back to a straight line between truck and pin so the customer still sees a connection instead of a blank map. The camera fits both points in frame and clamps the zoom so the truck never disappears off an edge.

live_tracking_view.dart Dart
// The truck's dot comes from the live document; the road it drives does not.
// Ask for a driving route between the driver's current GPS and the customer
// pin, and fall back to a straight line so the map is never left blank.
Future<List<LatLng>> routeToPin(LatLng driver, LatLng pin) async {
final result = await polylinePoints.getRouteBetweenCoordinates(
  googleApiKey: googleApiKey,
  request: PolylineRequest(
    origin: PointLatLng(driver.latitude, driver.longitude),
    destination: PointLatLng(pin.latitude, pin.longitude),
    mode: TravelMode.driving,
  ),
);

if (result.points.isEmpty) {
  // No route back (quota, transient error, unmapped lane): draw a direct
  // line so the customer still sees the truck heading their way.
  return [driver, pin];
}
return result.points.map((p) => LatLng(p.latitude, p.longitude)).toList();
}

What shipped first, and why

I ran design, engineering, and QA against a phased plan: authentication and the dashboard, then the calendar and scheduling, the pickup-request flow, history logging, and live tracking. Each phase was a checkpoint the client could see and sign off on, not a stage on the way to one big reveal. Design settled in Figma ahead of build so engineering worked against flows that were already decided.

The governing risk was scope on a brand-new two-sided product with no existing data to lean on. Leading with the generator side was the lever that contained it: a coherent, demoable product on a fixed timeline, with the provider app and deeper analytics staged as fast-follows once the core loop had been felt in real users’ hands. Because the two apps meet only at that one shared document, staging them apart cost nothing in rework.

Outcomes

Engineering Outcomes

1 codebase
one Flutter build ships iOS and Android
Real-time
truck streamed live onto the customer's map
Schedule-first
pickups booked by calendar and kept as history

Directorial Outcomes

Phase 1
generator app scoped to prove the core loop
On-time
demoable MVP inside a fixed timeline
Staged
provider app + analytics as fast-follows