Skip to main content

Mobile Structure

Vetty ships two Flutter apps that share architectural shape and diverge only where the user story demands it:

AppFolderAudienceSeed colour
vettyvetyy/Pet owners — their pets, vaccinations, share codes, nearby providersEmerald (0xFF059669)
vetdoctorvetdoctor/Clinic staff — today's appointments, scan-a-QR to adopt a petIndigo (0xFF4F46E5)

They target the same Laravel API (/api/v1) with Sanctum bearer tokens, reuse the same DI pattern, and share the same convention for feature folders. Everything else — themes, entry screen, feature mix — is app-specific.

Folder layout (shared shape)

<app>/
├── lib/
│ ├── config/env.dart # dart-define: apiBase, webBase
│ ├── core/
│ │ ├── api/api_client.dart # HTTP wrapper + ApiException
│ │ ├── auth/auth_store.dart # ChangeNotifier + shared_preferences
│ │ └── theme.dart # Material 3 theme via ColorScheme.fromSeed
│ ├── features/
│ │ ├── auth/ # login, forgot-password (vetty also has register)
│ │ ├── pets/ # vetty only — list, form, detail, vaccinations
│ │ ├── shares/ # vetty: generate QR • vetdoctor: scan + redeem
│ │ ├── providers/ # vetty only — directory search
│ │ ├── appointments/ # vetdoctor only — today's schedule
│ │ └── home/ # HomeShell (vetty) or DashboardScreen (vetdoctor)
│ └── main.dart # bootstraps AuthStore + ApiClient, roots the app
├── test/widget_test.dart # compile smoke test (theme builds)
└── pubspec.yaml

Core primitives

ApiClient (lib/core/api/api_client.dart)

A thin wrapper around package:http:

  • Bearer token injection — pulls auth.token from the injected AuthStore and sets Authorization: Bearer <token> on every request.
  • JSON envelope handling — unwraps {data: …} automatically but also tolerates flat responses.
  • Typed errors — non-2xx responses throw ApiException(message, statusCode) with the server's message when available.
  • 401 reaction — clears the AuthStore so the UI flips back to the login screen without a manual refresh.

Every feature service takes an ApiClient in its constructor:

class PetService {
PetService(this._api);
final ApiClient _api;
Future<List<Map<String, dynamic>>> list() async {
final res = await _api.get('/owner/me/pets');
// …
}
}

AuthStore (lib/core/auth/auth_store.dart)

A ChangeNotifier that owns auth state:

  • token — Sanctum bearer from /auth/login or /auth/owner/register.
  • user — the UserResource payload (id, name, email, account_type, etc.).
  • tenant — vetdoctor only; the TenantResource returned alongside the user so the app can scope /tenant/{id}/… calls.

It persists to shared_preferences under namespaced keys (vetty.auth.token / vetdoctor.auth.token etc.) and calls notifyListeners() whenever any of the three fields change. main.dart listens on the store and swaps between the login screen and the home screen:

class _RootState extends State<_Root> {
@override
void initState() {
super.initState();
widget.auth.addListener(_onAuthChange);
}

@override
Widget build(BuildContext context) {
if (!widget.auth.isAuthenticated) {
return LoginScreen(auth: widget.authService);
}
return HomeShell(/* or DashboardScreen */);
}
}

Env (lib/config/env.dart)

Build-time configuration via String.fromEnvironment:

flutter run \
--dart-define=VETTY_API_BASE=https://api.vetty.example/api/v1 \
--dart-define=VETTY_WEB_BASE=https://app.vetty.example

Defaults target the Android emulator loopback (http://10.0.2.2:8000/api/v1). Shipped builds must override.

Feature service pattern

Every feature folder has a *_service.dart that is the only place that talks to ApiClient. Screens and widgets call the service, never the client:

features/pets/
├── pet_service.dart # list, create, update, vaccinations, addVaccination
├── pets_screen.dart # FutureBuilder + FAB + tile-per-pet
├── pet_form_screen.dart # create/edit modal
└── pet_detail_screen.dart # summary + vaccinations list

This keeps screens deterministic in tests (swap the service with a fake) and keeps HTTP concerns out of the widget tree.

Dependencies

Both apps intentionally depend on very little:

PackageVersionUsed byPurpose
http^1.2.2bothREST client
shared_preferences^2.3.2bothpersist AuthStore
intl^0.19.0bothdate formatting
url_launcher^6.3.0bothopen reset-password / provider links
qr_flutter^4.1.0vettyrender the share QR
mobile_scanner^5.2.3vetdoctorscan QR + manual-code fallback

No state-management library, no code-gen — just ChangeNotifier, StatefulWidget, FutureBuilder and RefreshIndicator.

Endpoint paths

The Flutter apps hit the exact same /api/v1 surface as the web SPA. Owner-scoped routes live under /owner/; clinic-scoped routes under /tenant/{id}/:

AppConcernPath(s)
vettyAuthPOST /auth/login, POST /auth/owner/register, GET /auth/me, POST /auth/forgot-password, GET /auth/verify-reset-token, POST /auth/reset-password, POST /auth/logout
vettyPetsGET|POST /owner/me/pets, PATCH /owner/me/pets/{id}, GET|POST /owner/me/pets/{id}/vaccinations
vettySharesGET|POST /owner/me/shares, POST /owner/me/shares/{id}/revoke
vettyProvidersGET /providers?q=&business_type=&latitude=&longitude=&radius_km=
vetdoctorAuthPOST /auth/login, POST /auth/forgot-password, POST /auth/logout
vetdoctorSharesPOST /shares/redeem { code, tenant_id, user_id }, GET /shares/{code}
vetdoctorAppointmentsGET /tenant/{id}/appointments?from=<iso>&to=<iso>

See the API reference for the full parameter list and response shapes.

What each app adds on top of the shared shape

  • vetty — a HomeShell with a bottom NavigationBar (Pets, Nearby) and a profile bottom sheet. An InheritedWidget (VettyServices) makes the services available to deep descendants so the QR share screen can read the pet + share services without prop-drilling.
  • vetdoctor — a single-screen dashboard (no nav bar), a big "Scan share" FAB, and an extended AuthStore that stores tenant so appointments + redeem can scope their calls.

Deep-dive pages: vetty, vetdoctor.