Documenta flusso dati per feature, QR e update checker per chi sviluppa il progetto; riorganizza il README nella struttura standard overview/features/architecture/requisiti/installazione/uso/test/build/ contribuire, senza perdere contenuti esistenti. Piccole correzioni a CLAUDE.md (app_theme.dart nell'albero, nota su kAppVersion non sincronizzato). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.6 KiB
Architettura — BitePlan
Documento tecnico per chi sviluppa o mantiene l'app: come sono strutturati i dati e come scorrono tra UI, provider e persistenza. Per l'elenco file per file vedi CLAUDE.md; per l'uso dell'app vedi guida-utente.md.
Pattern generale
Ogni feature (meal_planner, converter, shopping_list) segue lo stesso schema:
Widget (presentation/) → Provider (ChangeNotifier) → StorageService / asset → SharedPreferences
↑____________________________notifyListeners()____________________________|
Ogni provider espone metodi che mutano lo stato immutabile in memoria, chiamano
notifyListeners() e poi salvano in modo fire-and-forget (senza await, l'UI non aspetta
la scrittura su disco). load() va chiamato una volta all'avvio (in main.dart) prima di
usare il provider.
Piano pasti (meal_planner)
- Modello:
MealPlanè una mappadayId → DayPlan;DayPlanha tre liste di stringhe (colazione,pranzo,cena). Entrambi sono immutabili: ogni modifica passa dacopyWith/withUpdatedDaye ricostruisce l'oggetto. - Provider:
MealPlannerProvidertiene_planin memoria, la inizializza vuota conMealPlan.empty(kDayIds), la carica daStorageService(chiavemeals, JSON viatoJsonString/fromJsonString). Se il JSON salvato è corrotto, torna a un piano vuoto invece di lanciare un'eccezione. - Condivisione QR (
qr_codec.dart):buildQrPayloadserializza il piano come{ "v": 1, "meals": {...} }e restituiscenullse supera 2953 byte (capienza QR con error correction L) — la UI mostra un errore in quel caso.parseMealPlanFromQrvalida struttura e versione prima di ricostruire ilMealPlan, per non far crashare l'app su un QR malformato o generato da una versione futura. - Generazione lista spesa: la pagina piano pasti legge
plan.allItemse chiamaShoppingListProvider.addAll(...)— l'aggregazione duplicati avviene lì, non nel meal planner.
Convertitore crudo/cotto (converter)
- Dati:
assets/data/conversions.jsonè una mappaalimento → metodo → { yield }, caricata una sola volta inConverterProvider.loadDb()e appiattita in una lista diConversionEntry { food, method, yieldFactor }. - Calcolo:
rawToCooked = raw * yieldFactor,cookedToRaw = cooked / yieldFactor. Il provider arrotonda il risultato a un decimale ((v * 10).roundToDouble() / 10). - Ricerca:
search(query)filtra_dbper sottostringa case-insensitive sul nome alimento; non c'è persistenza, lo stato (selected,grams,rawToCooked) vive solo in memoria e si azzera conreset(). - I coefficienti e le fonti sono documentati in conversioni.md — per aggiungerne di nuovi si modifica solo il JSON, non serve toccare il provider.
Lista della spesa (shopping_list)
- Modello:
ShoppingItem { id, name, checked, quantity }.idè generato cometimestamp_contatore(non un vero UUID) per garantire unicità anche ad aggiunte multiple nello stesso millisecondo. - Provider:
ShoppingListProviderpersiste su chiaveshopping_listcome lista JSON.pendingItems/checkedItemssono getter derivati, non stato separato. addAll(names): raggruppa i nomi per confronto case-insensitive, somma le occorrenze inquantitye salta quelli già presenti nella lista (case-insensitive anche lì) — è il meccanismo dietro "zucchine (x2)" e dietro il "senza duplicati" della generazione lista dal piano pasti.
Controllo aggiornamenti
UpdateService.checkUpdate() (chiamato da app.dart in initState) interroga
GET /repos/davide3011/biteplan/releases/latest su GitHub con dart:io HttpClient
(nessun package HTTP aggiuntivo), timeout 5s su connessione e risposta. Confronta il
tag_name (spogliato del prefisso v) con kAppVersion via confronto semver a tre
componenti (isNewer); qualsiasi errore di rete, parsing o timeout fa fallire silenziosamente
il controllo (ritorna null, nessun dialog). Disabilitato su web (kIsWeb).
kAppVersion in app_constants.dart non è sincronizzato automaticamente con
pubspec.yaml — va aggiornato a mano ad ogni release, altrimenti il confronto versione è
sbagliato.
MethodChannel nativo
Il bottone "Scarica" del dialog di aggiornamento e altri link esterni passano da
UrlLauncherService, che invoca il canale nativo com.davide.biteplan/launcher
implementato in Kotlin (android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt)
con un Intent.ACTION_VIEW — scelta per evitare di aggiungere il package url_launcher
per un solo caso d'uso.