diff --git a/CLAUDE.md b/CLAUDE.md index e2f987e..20ddca9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,113 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -BitePlan è un'app Android per meal planning, conversione crudo/cotto e lista della spesa, scritta in Flutter. Tutta la UI e la UX sono in **italiano**. L'obiettivo finale è un APK Android buildabile via Docker. +BitePlan è un'app Android per meal planning, conversione crudo/cotto e lista della spesa, scritta in Flutter. Tutta la UI e la UX sono in **italiano**. Il rilascio avviene come APK Android buildato via Docker. + +## Ruolo atteso + +Quando viene richiesta una modifica o una nuova funzionalità, segui questo flusso: + +1. **Analizza prima di implementare**: valuta se la richiesta ha senso — fattibilità, complessità reale, impatto su architettura/UX/persistenza. Se esiste un modo migliore di implementarla, proponilo; se la richiesta è mal posta o ha un costo sproporzionato al beneficio, dillo esplicitamente con l'alternativa. Motiva i giudizi con riferimenti concreti al codice. +2. **Implementa da esperto**: se la richiesta va bene (o dopo l'eventuale discussione), implementala seguendo le convenzioni del progetto (feature-first, Provider, tutto in italiano). +3. **Suggerisci i test, non scriverli**: al termine indica quali test coprirebbero la modifica (casi e file dove andrebbero), ma implementali solo se richiesto esplicitamente. ## Comandi ```bash -# Sviluppo (web server con hot reload) -cd docker/dev && docker compose up -# → http://localhost:5173 -# Con il container attivo: docker compose attach dev → r = hot reload +# Sviluppo (host, no Docker) +emulator -avd biteplan & # AVD dedicato, Pixel 6 / Android 14 (google_apis x86_64) +flutter run # hot reload con "r", hot restart con "R" -# Test (dalla root del progetto — richiede immagine biteplan-build) +# Analisi statica +flutter analyze # lint: package:flutter_lints/flutter.yaml + +# Test rapidi (host) +flutter test # tutta la suite +flutter test test/features/meal_planner/qr_test.dart # singolo file + +# Test riproducibili (Docker, immagine biteplan-build — usati come riferimento) docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \ bash -c "flutter pub get --enforce-lockfile && flutter test" -# Test singolo file -docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \ - bash -c "flutter test test/features/meal_planner/qr_test.dart" +# Integration test (richiede emulatore avviato) +flutter test integration_test/app_test.dart -# Build APK (headless, da host) -bash docker/build/build.sh # debug → dist/biteplan-debug.apk -export BITEPLAN_KEYSTORE_PASS=password # richiesto solo per --release -bash docker/build/build.sh --release # release → dist/biteplan-release.apk +# Build APK (headless, via Docker) +bash docker/build.sh # debug → dist/biteplan-debug.apk +export BITEPLAN_KEYSTORE_PASS=password # richiesto solo per --release +bash docker/build.sh --release # release → dist/biteplan-release.apk ``` +## Sviluppo locale + +- **Flutter host**: installato in `~/flutter` (tag `3.41.9`, stessa versione pinnata nel Dockerfile), nel `PATH` via `~/.bashrc`. Docker non si usa più per il dev; resta per test di riferimento e build APK (isolamento, `--enforce-lockfile`, firma release). +- **Emulatore**: Android SDK in `/home/davide/android-sdk`; accelerazione hardware via `/dev/kvm` (WSL2 con KVM abilitato). +- **AVD `biteplan`**: dedicato a questo progetto (Pixel 6, Android 14, google_apis/x86_64). Non riutilizzare AVD di altri progetti (es. `palladium_wallet`). +- **Permessi**: le build Docker girano come root nel volume montato — se `.dart_tool/` o `build/` risultano di proprietà root, serve `sudo chown -R davide:davide .dart_tool build` prima di usare flutter da host. + ## Build e firma -- **Flutter pinnato**: versione `3.41.9` nei Dockerfile (`ARG FLUTTER_VERSION`); aggiornare anche `.flutter-version` -- **Gradle cache**: named volume Docker `gradle-cache`, isolata dall'host +- **Flutter pinnato**: versione `3.41.9` in `docker/Dockerfile` (`ARG FLUTTER_VERSION`); aggiornare anche `.flutter-version` e l'install host - **Dipendenze**: `pubspec.lock` committato; la build usa `--enforce-lockfile` e fallisce se il lock non è allineato -- **Keystore**: `docker/biteplan.jks` è in `.gitignore`; da ottenere dall'autore o generare con `keytool` -- **Firma release**: password passata via `BITEPLAN_KEYSTORE_PASS` (mai hardcoded); alias keystore: `biteplan` +- **Keystore**: `docker/biteplan.jks` è in `.gitignore`; da ottenere dall'autore o generare con `keytool`. Password via `BITEPLAN_KEYSTORE_PASS` (mai hardcoded); alias: `biteplan` - **applicationId**: `com.davide.biteplan` — non cambiare, garantisce aggiornamento diretto dalla v1.2.1 - **versionCode**: letto da `pubspec.yaml` (`version: X.Y.Z+BUILD`); BUILD deve essere sempre crescente ## Architettura +Feature-first sotto `lib/features/` (meal_planner, converter, shopping_list, guide), ognuna con `models/`, `providers/`, `presentation/`. Codice trasversale in `lib/shared/` e `lib/core/`. + ``` lib/ -├── main.dart -├── app.dart # MaterialApp, NavigationBar; initState chiama UpdateService.checkUpdate() e mostra showUpdateDialog() se c'è aggiornamento -├── core/ -│ ├── constants/app_constants.dart # kDayIds, kMealSlots, kStorageKey*, kAppVersion -│ └── theme/app_theme.dart +├── app.dart # MaterialApp + NavigationBar; in initState chiama UpdateService.checkUpdate() e mostra showUpdateDialog() +├── core/constants/app_constants.dart # kDayIds, kMealSlots, kStorageKey*, kAppVersion ├── shared/ -│ ├── services/ -│ │ ├── storage_service.dart # wrapper SharedPreferences (load/save) -│ │ ├── update_service.dart # checkUpdate() → GitHub API, isNewer(), parseTagName() (@visibleForTesting) -│ │ └── url_launcher_service.dart # MethodChannel 'com.davide.biteplan/launcher' → Intent.ACTION_VIEW -│ └── widgets/ -│ └── update_dialog.dart # showUpdateDialog() — AlertDialog con "Più tardi" / "Scarica" +│ ├── services/storage_service.dart # wrapper SharedPreferences (load/save) +│ ├── services/update_service.dart # checkUpdate() → GitHub API; isNewer(), parseTagName() (@visibleForTesting) +│ ├── services/url_launcher_service.dart # MethodChannel 'com.davide.biteplan/launcher' → Intent.ACTION_VIEW +│ └── widgets/update_dialog.dart # showUpdateDialog() — "Più tardi" / "Scarica" ├── features/ -│ ├── meal_planner/ -│ │ ├── models/meal_plan.dart # MealPlan, DayPlan -│ │ ├── providers/meal_planner_provider.dart -│ │ ├── qr_codec.dart # buildQrPayload, parseMealPlanFromQr -│ │ └── presentation/ -│ │ ├── pages/meal_planner_page.dart -│ │ ├── pages/qr_scan_page.dart -│ │ └── widgets/meal_card.dart, qr_share_sheet.dart -│ ├── converter/ -│ │ ├── models/conversion_entry.dart # rawToCooked, cookedToRaw (metodi sul model) -│ │ ├── providers/converter_provider.dart -│ │ └── presentation/pages/converter_page.dart -│ ├── shopping_list/ -│ │ ├── models/shopping_item.dart # quantity per aggregazione duplicati -│ │ ├── providers/shopping_list_provider.dart -│ │ └── presentation/ -│ │ ├── pages/shopping_list_page.dart -│ │ └── widgets/shopping_item_tile.dart -│ └── guide/ -│ └── presentation/ -│ ├── pages/guide_page.dart # 3 tab: Pasti, Converti, Spesa -│ └── widgets/info_bottom_sheet.dart # aperto dal bottone info in AppBar -└── assets/data/conversions.json # 50+ alimenti × metodi cottura +│ ├── meal_planner/ # MealPlan/DayPlan, qr_codec.dart (buildQrPayload, parseMealPlanFromQr), pagine planner + scan QR +│ ├── converter/ # ConversionEntry con rawToCooked/cookedToRaw, dati da assets/data/conversions.json +│ ├── shopping_list/ # ShoppingItem con quantity per aggregazione duplicati +│ └── guide/ # guide_page (3 tab) + info_bottom_sheet +└── assets/data/conversions.json # 50+ alimenti × metodi cottura ``` -**State management**: Provider (`ChangeNotifier`). -**Persistenza**: `shared_preferences`, chiavi `meals` e `shopping_list` (JSON serializzato). -**Conversione**: logica in `ConversionEntry` — `rawToCooked = raw * yieldFactor`, `cookedToRaw = cooked / yieldFactor`. -**UI**: Material 3, seed color `Color(0xFF2d6a4f)`, tutto in italiano. -**QR**: payload JSON `{ "v": 1, "meals": { ... } }`, limite 2953 byte (capacità QR con error correction L). -**Update checker**: `UpdateService.checkUpdate()` usa `dart:io` `HttpClient` (nessun package aggiuntivo); disabilitato su web (`kIsWeb`). Il `MethodChannel` `com.davide.biteplan/launcher` è implementato in `android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt`. -**Widget test con MethodChannel**: usare `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler` per mockare il canale nei test — vedi `test/shared/widgets/update_dialog_test.dart`. +**State management**: Provider (`ChangeNotifier`), uno per feature, registrati in `main.dart` con `MultiProvider`. +**Persistenza**: `shared_preferences`, chiavi `meals` e `shopping_list` (JSON serializzato). +**Conversione**: logica in `ConversionEntry` — `rawToCooked = raw * yieldFactor`, `cookedToRaw = cooked / yieldFactor`. +**UI**: Material 3, seed color `Color(0xFF2d6a4f)`, tutto in italiano. +**QR**: payload JSON `{ "v": 1, "meals": { ... } }`, limite 2953 byte (capacità QR con error correction L). +**Update checker**: `UpdateService.checkUpdate()` usa `dart:io` `HttpClient` (nessun package aggiuntivo); disabilitato su web (`kIsWeb`). Il `MethodChannel` `com.davide.biteplan/launcher` è implementato in `android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt`. ## Testing -130 test (unit + widget). Non richiedono device fisico. +130 test unit + widget in `test/` (rispecchia la struttura di `lib/`), più un integration test in `integration_test/app_test.dart`. I test unit/widget non richiedono device. -``` -test/ -├── helpers/pump_app.dart # estensione pumpApp per widget test -├── features/ -│ ├── converter/ -│ │ ├── models/conversion_entry_test.dart -│ │ └── providers/converter_provider_test.dart -│ ├── meal_planner/ -│ │ ├── models/meal_plan_test.dart -│ │ ├── providers/meal_planner_provider_test.dart -│ │ ├── widgets/meal_card_test.dart -│ │ └── qr_test.dart -│ └── shopping_list/ -│ ├── models/shopping_item_test.dart -│ ├── providers/shopping_list_provider_test.dart -│ └── widgets/shopping_item_tile_test.dart -└── shared/ - ├── services/update_service_test.dart - └── widgets/update_dialog_test.dart -``` - -I test usano `SharedPreferences.setMockInitialValues({})` per isolare lo storage. +- Storage isolato con `SharedPreferences.setMockInitialValues({})` +- Widget test: estensione `pumpApp` in `test/helpers/pump_app.dart` +- MethodChannel nei test: `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler` — vedi `test/shared/widgets/update_dialog_test.dart`