feat(android): implement full Android frontend

- Build config: build.gradle (app + root), settings.gradle, AndroidManifest
- Application entry: PantreeApplication (Hilt), MainActivity (Compose host)
- Data layer:
  - ApiService (Retrofit interface, all 20 endpoints)
  - ApiModels (all request/response DTOs)
  - AuthInterceptor (JWT Bearer header injection)
  - TokenStore (EncryptedSharedPreferences)
  - SyncPreferences (DataStore, last-sync timestamp)
  - PantreeDatabase (Room, 5 entities)
  - Entities: PantryItemEntity, RecipeEntity, RecipeIngredientEntity, ShoppingListEntity, ShoppingListItemEntity
  - DAOs: PantryDao, RecipeDao, ShoppingListDao
  - Repositories: AuthRepository, PantryRepository, RecipeRepository, ShoppingListRepository, SyncRepository
- DI: AppModule (Hilt, provides OkHttp/Retrofit/Room/DAOs)
- Util: Result<T> sealed class, safeApiCall, toUserMessage (human-readable error mapping)
- Sync: SyncManager (DefaultLifecycleObserver, triggers on app open/foreground)
- Theme: Color, Type, Theme (Material 3, light + dark)
- Navigation: Screen sealed class, PantreeNavGraph (deep links for password reset)
- Shared components: LoadingState, ErrorState, EmptyState, OfflineBanner, PantreeTopBar
- Auth screens: SplashScreen, SignInScreen, SignUpScreen, ForgotPasswordScreen, ResetPasswordScreen + AuthViewModel
- Pantry screen: PantryScreen (list/add/edit/delete dialogs) + PantryViewModel
- Recipe screens: RecipesScreen (filter chips, search, availability badges) + RecipeDetailScreen (scale 1x/2x/3x, ingredient pantry status) + RecipeViewModel
- Shopping screens: ShoppingListsScreen + ShoppingListDetailScreen (check-off, progress bar, merge feedback, add-from-recipes) + ShoppingListViewModel
- Settings screen: sign out + account deletion (15-day window copy) + SettingsViewModel
- Tests: ResultTest (unit), RecipeDetailScreenTest (unit), PantryScreenTest (instrumented)
- README: architecture, module structure, UI states table, sync strategy, setup

Every screen handles loading / error / empty / success states.
Offline: cached Room data shown read-only.
This commit is contained in:
Azriel
2026-05-10 15:09:12 +00:00
parent e633d693da
commit 75db2ea8f5
51 changed files with 4138 additions and 0 deletions

76
android/README.md Normal file
View File

@@ -0,0 +1,76 @@
# Pantree Android
Android client for Pantree — the app that tells you what you can cook with what you already have.
## Architecture
- **MVVM** — ViewModel + StateFlow, no LiveData
- **Hilt** — dependency injection
- **Room** — local cache (pantry items, recipes, shopping lists)
- **Retrofit + OkHttp** — network layer with JWT interceptor
- **Jetpack Compose** — declarative UI, Material 3
- **EncryptedSharedPreferences** — JWT stored at rest
- **DataStore** — last-sync timestamp
## Module Structure
```
app/src/main/java/com/pantree/app/
├── data/
│ ├── local/ # Room DB, DAOs, entities, TokenStore, SyncPreferences
│ ├── model/ # API DTOs (request/response)
│ ├── remote/ # ApiService (Retrofit), AuthInterceptor
│ └── repository/ # AuthRepository, PantryRepository, RecipeRepository,
│ # ShoppingListRepository, SyncRepository
├── di/ # Hilt AppModule
├── sync/ # SyncManager (lifecycle observer)
├── ui/
│ ├── components/ # LoadingState, ErrorState, EmptyState, OfflineBanner, PantreeTopBar
│ ├── navigation/ # Screen sealed class, PantreeNavGraph
│ ├── screens/
│ │ ├── auth/ # SplashScreen, SignInScreen, SignUpScreen,
│ │ │ # ForgotPasswordScreen, ResetPasswordScreen + AuthViewModel
│ │ ├── pantry/ # PantryScreen + PantryViewModel
│ │ ├── recipe/ # RecipesScreen, RecipeDetailScreen + RecipeViewModel
│ │ ├── shopping/ # ShoppingListsScreen, ShoppingListDetailScreen + ShoppingListViewModel
│ │ └── settings/ # SettingsScreen + SettingsViewModel
│ └── theme/ # Color, Type, Theme
└── util/ # Result<T>, safeApiCall, toUserMessage
```
## UI States
Every screen handles all four states:
| State | Implementation |
|-------|---------------|
| **Loading** | `LoadingState` composable — spinner + contextual message |
| **Error** | `ErrorState` composable — emoji + message + retry button |
| **Empty** | `EmptyState` composable — emoji + title + subtitle + optional CTA |
| **Success** | Full content with `LazyColumn` / detail view |
Offline: cached data shown read-only, `OfflineBanner` displayed.
## Sync Strategy
- `SyncManager` implements `DefaultLifecycleObserver` — triggers on `onStart` (app open + foreground)
- Delta sync via `GET /sync?since=<last_timestamp>`
- Server timestamp stored in DataStore, used as `since` on next sync
- Room cache updated; UI observes `Flow<List<Entity>>` — updates automatically
## Setup
1. Set `BASE_URL` in `app/build.gradle` debug/release buildConfigFields
2. Set `GOOGLE_WEB_CLIENT_ID` for Google Sign-In
3. Run backend (`npm run dev` from repo root)
4. `./gradlew assembleDebug`
## Running Tests
```bash
# Unit tests
./gradlew test
# Instrumented tests (requires emulator/device)
./gradlew connectedAndroidTest
```