Documentation

dfw_errand — errand marketplace

Walkthrough of the dfw_errand Flutter app — a customer + driver marketplace for delivery and errands.

dfw_errand is a two-sided marketplace: customers post errands, drivers pick them up. The app supports both roles in a single binary, switching mode based on a profile flag. Repo: /Users/imzee/projects/appmint_go/dfw_errand/.

It's the cleanest example of using the logistics module end to end, plus dual-role auth in the same app.

What it uses

ModuleEndpoints
Profile/profile/customer/signin
Logistics/client/logistics/init, /jobs, /jobs/:id, /jobs/:id/messages, /quote, /payouts/*
Storefront/storefront/stripe/intent (customer payment), Stripe SDK
Repository/repository/find/job (client-side job history)

The driver side hits /client/logistics/jobs/available, /jobs/:id/accept, location updates, and the payout endpoints. The customer side hits the regular job-creation, tracking, and payment endpoints.

Project layout

dfw_errand/lib/
├── services/
│   ├── api_service.dart        # Generic AppEngine wrapper
│   ├── http_client.dart        # Singleton with `useUserToken` flag
│   ├── delivery_service.dart   # Logistics endpoints — dual customer/driver
│   ├── finance_service.dart    # Payout / earnings (driver)
│   ├── location_service.dart   # GPS for driver location updates
│   ├── repository_service.dart # Generic /repository/* CRUD
│   ├── storage_service.dart    # JWT + role
│   └── stripe_service.dart     # Customer payment
├── providers/
└── screens/

delivery_service.dart is the meatiest file — every customer and driver endpoint wrapped in a typed method. Worth reading top to bottom.

Files to read first

  • lib/services/delivery_service.dart — the full set of /client/logistics/* calls. Includes a setMode('customer'|'driver') switch that adjusts which endpoints the service uses.
  • lib/services/location_service.dart — uses geolocator to push driver coordinates to /client/logistics/location every N seconds while online.
  • lib/screens/customer/track_job_screen.dart — polling pattern for status updates (5s interval, stop on terminal state). Mirrors the tracking recipe.
  • lib/providers/app_mode_provider.dart — the customer/driver toggle state, read by service classes.

Patterns worth lifting

Dual-role single binary

The same APK serves both customers and drivers. A single profile field (isDriver) determines which navigation tree the app shows after sign-in. Both sides share auth, the HTTP client, and most plumbing.

// pseudocode
if (user.isDriver) {
  Navigator.pushReplacementNamed(context, '/driver-home');
} else {
  Navigator.pushReplacementNamed(context, '/customer-home');
}

This avoids two app store listings and two codebases. The trade-off: the app bundle includes screens neither user will see (drivers don't need the customer order placement screen and vice versa). For most marketplaces that's an acceptable cost.

Background location updates

The driver side runs a foreground service while online. geolocator + flutter_background_service keeps location updates flowing even when the app is backgrounded.

Geolocator.getPositionStream(
  locationSettings: const LocationSettings(
    accuracy: LocationAccuracy.high,
    distanceFilter: 25, // meters
  ),
).listen((position) {
  appmintHttp.put('/client/logistics/location', body: {
    'lat': position.latitude,
    'lng': position.longitude,
    'heading': position.heading,
    'speed': position.speed,
  });
});

A 25-meter filter prevents the device from spamming updates while stopped at lights.

Quote-before-book

Customers can preview a price before booking:

final quote = await appmintHttp.post('/client/logistics/quote', body: {
  'pickup': pickupAddr,
  'dropoff': dropoffAddr,
  'weight': estimatedWeight,
});
// quote.estimatedPrice

This pattern is reusable for any pricing-via-distance service. The endpoint runs the same calculation the actual booking does, so a quote followed by a book at the same prices is a stable contract.

What it doesn't do

  • No biometrics — drivers don't want a face scan when they need to accept a job fast.
  • No offline cache for jobs — being offline-but-still-displaying-stale-jobs is misleading. Better to surface "you are offline."
  • No push for new jobs (yet) — drivers refresh the available-jobs list manually. A future version will add FCM push for instant notification when a matching job appears.

Where to fork from

If you're building a delivery/errand/services marketplace:

  1. Copy lib/services/{http_client,api_service,delivery_service,location_service}.dart.
  2. Adapt delivery_service to your domain (replace jobs with tasks, tickets, requests).
  3. Keep the app_mode_provider pattern — most marketplaces are dual-role.

Reference docs to pair with this example