Mobile Structure
Vetty ships two Flutter apps that share architectural shape and diverge only where the user story demands it:
| App | Folder | Audience | Seed colour |
|---|---|---|---|
| vetty | vetyy/ | Pet owners — their pets, vaccinations, share codes, nearby providers | Emerald (0xFF059669) |
| vetdoctor | vetdoctor/ | Clinic staff — today's appointments, scan-a-QR to adopt a pet | Indigo (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.tokenfrom the injectedAuthStoreand setsAuthorization: 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'smessagewhen 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/loginor/auth/owner/register.user— theUserResourcepayload (id, name, email, account_type, etc.).tenant— vetdoctor only; theTenantResourcereturned 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:
| Package | Version | Used by | Purpose |
|---|---|---|---|
http | ^1.2.2 | both | REST client |
shared_preferences | ^2.3.2 | both | persist AuthStore |
intl | ^0.19.0 | both | date formatting |
url_launcher | ^6.3.0 | both | open reset-password / provider links |
qr_flutter | ^4.1.0 | vetty | render the share QR |
mobile_scanner | ^5.2.3 | vetdoctor | scan 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}/:
| App | Concern | Path(s) |
|---|---|---|
| vetty | Auth | POST /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 |
| vetty | Pets | GET|POST /owner/me/pets, PATCH /owner/me/pets/{id}, GET|POST /owner/me/pets/{id}/vaccinations |
| vetty | Shares | GET|POST /owner/me/shares, POST /owner/me/shares/{id}/revoke |
| vetty | Providers | GET /providers?q=&business_type=&latitude=&longitude=&radius_km= |
| vetdoctor | Auth | POST /auth/login, POST /auth/forgot-password, POST /auth/logout |
| vetdoctor | Shares | POST /shares/redeem { code, tenant_id, user_id }, GET /shares/{code} |
| vetdoctor | Appointments | GET /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
HomeShellwith a bottomNavigationBar(Pets, Nearby) and a profile bottom sheet. AnInheritedWidget(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
AuthStorethat storestenantso appointments + redeem can scope their calls.