Scadoo
La dispensa, sotto controllo.
App mobile per frigo, dispensa e freezer: scansioni il codice a barre, segni la scadenza e Scadoo ti avvisa in tempo. Lista della spesa da condividere in un tocco. Funziona anche offline.
In sviluppo · iOS e Android · progetto personale 2025
Perché esiste
È nata da un problema di tutti i giorni: cibo che scade senza che te ne accorga, liste della spesa dimenticate, nessun modo semplice per sapere quante confezioni di pasta restano.
Le app che esistono chiedono un abbonamento o sono piene di cose. L'obiettivo di Scadoo è fare poche cose, farle bene e farle in fretta: scansioni il prodotto, l'app recupera nome e marca da Open Food Facts, aggiungi la scadenza e hai finito. Due tocchi.
La lista della spesa è pensata per la famiglia: un pulsante la condivide su WhatsApp o iMessage, già ordinata e pronta per il supermercato.
- Piattaforme
- iOS · Android
- Framework
- Flutter · Dart
- Database
- SQLite (sqflite)
- Stato
- Provider · ChangeNotifier
- Dati prodotti
- Open Food Facts
- Notifiche
- flutter_local_notifications
Cosa fa
Otto funzioni, tutte pensate per richiedere il minimo indispensabile di tocchi.
Scansione rapida
Codice a barre, ricerca del prodotto, scheda già compilata. In dispensa con due tocchi.
Open Food Facts
Prima il catalogo italiano, poi quello mondiale. Recupera nome, marca, categoria e immagine.
Scadenze a colori
Verde finché va tutto bene, poi giallo, arancione e rosso man mano che la data si avvicina.
Frigo, dispensa, freezer
Tre luoghi separati, filtrabili dall'inventario.
Notifiche intelligenti
Ogni mattina alle 9 un riepilogo delle scadenze, generato sul telefono.
Lista della spesa
Spunti, riordini, aggiungi a mano e condividi con un tocco.
Quantità al volo
Più e meno direttamente sulla scheda. A zero, il prodotto sparisce da solo.
Prima di tutto offline
Tutto vive su SQLite nel telefono. Funziona senza connessione: la rete serve solo per cercare i prodotti.
Dal codice a barre alla dispensa
Il percorso che ho voluto più corto possibile.
Scansiona
La fotocamera legge il codice a barre.
Riconosce
Open Food Facts restituisce nome, marca e categoria.
Data
Aggiungi la scadenza e il luogo.
Ti avvisa
Il colore cambia e la notifica arriva in tempo.
- Tutto bene
- Entro 7 giorni
- Entro 3 giorni
- Scaduto
Com'è fatta
Il codice è diviso in strati con responsabilità chiare. Ogni strato dipende solo da quello sotto.
lib/ ├── main.dart ← avvio, Provider, barra in basso ├── theme/ │ └── app_theme.dart ← colori, tipografia, expiryColor() ├── models/ ← solo dati, nessuna dipendenza da Flutter │ ├── product.dart ← daysToExpiry, isExpired, toMap/fromMap │ └── shopping_item.dart ├── services/ ← I/O, rete, sistema: niente interfaccia │ ├── database_service.dart ← SQLite singleton, CRUD │ ├── notification_service.dart │ └── openfoodfacts_service.dart ├── providers/ ← stato globale, chiama i servizi │ ├── inventory_provider.dart │ └── shopping_list_provider.dart ├── screens/ ← una schermata, un file │ ├── home_screen.dart │ ├── inventory_screen.dart │ ├── scanner_screen.dart │ ├── add_product_screen.dart │ └── shopping_list_screen.dart └── widgets/ └── product_card.dart ← swipe, quantità, badge scadenza
Le dipendenze, e perché
mobile_scanner
Lo scanner di codici a barre più veloce per Flutter. Gestisce messa a fuoco, torcia e un mirino disegnato con CustomPainter.
sqflite + path
SQLite nativo, completamente offline. Due tabelle: prodotti e voci della spesa.
flutter_local_notifications
Notifiche programmate sul telefono, senza server: riepilogo alle 9 e avviso all'apertura.
http
Le chiamate REST a Open Food Facts, prima l'endpoint italiano e poi quello mondiale.
provider
ChangeNotifier per inventario e lista della spesa. Semplice, leggibile, senza generazione di codice.
flutter_slidable
Scorri per eliminare o modificare un prodotto, come ci si aspetta su mobile.
share_plus
Condivide la lista con il menu di sistema (WhatsApp, iMessage…) senza backend.
uuid
Identificativi unici per prodotti e voci, per evitare collisioni in SQLite.
Due pezzi di codice
La scadenza vive nel modello
La logica delle scadenze sta nel modello, senza dipendenze da Flutter: facile da testare e da riusare.
Dartlib/models/product.dart
class Product { final DateTime? expiryDate; int get daysToExpiry { if (expiryDate == null) return 9999; final today = DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day); final expiry = DateTime(expiryDate!.year, expiryDate!.month, expiryDate!.day); return expiry.difference(today).inDays; } bool get isExpired => expiryDate != null && daysToExpiry < 0; bool get isExpiringSoon => expiryDate != null && daysToExpiry <= 3; bool get isExpiringThisWeek => expiryDate != null && daysToExpiry <= 7; }
Open Food Facts, prima in Italia
Il servizio prova prima l'endpoint italiano, più completo sui prodotti locali, poi quello mondiale. La risposta diventa un oggetto OpenFoodFactsResult indipendente dal JSON dell'API.
Dartlib/services/openfoodfacts_service.dart
Future<OpenFoodFactsResult> fetchProduct(String barcode) async { for (final base in [ 'https://it.openfoodfacts.org', 'https://world.openfoodfacts.org', ]) { final result = await _fetch('$base/api/v0/product/$barcode.json'); if (result.found) return result; } return const OpenFoodFactsResult(found: false); }
Problemi incontrati
Aprili per vedere la soluzione che ho adottato.
Doppia scansione accidentale
Il callback onDetect viene chiamato più volte per lo stesso codice nel giro di millisecondi. Soluzione: un flag _processing che blocca le chiamate successive finché la navigazione non è finita.
Giorni alla scadenza: fuso orario e mezzanotte
Calcolare la differenza in ore dà risultati sbagliati vicino alla mezzanotte. Soluzione: normalizzare entrambe le date a DateTime(anno, mese, giorno) prima di contare i giorni interi.
Dati italiani di qualità variabile
Molti prodotti italiani hanno il nome in inglese o vuoto. Soluzione: una cascata di ripiego, product_name_it → product_name → generic_name_it → generic_name. Se sono tutti vuoti, l'app mostra il codice e l'utente scrive il nome.
Notifiche da ricalcolare a ogni modifica
Ogni aggiunta, modifica o eliminazione cambia le notifiche programmate. Soluzione: cancelAll() e nuova programmazione dentro loadProducts() del provider, così il ciclo è automatico.
Percorso del database tra iOS e Android
Il percorso del database cambia tra le due piattaforme. Soluzione: getDatabasesPath() di sqflite insieme a path.join(), che costruisce il percorso giusto su entrambe.
Avanzamento
11 di 15 completati
- Architettura modelli, servizi, provider, schermateFatto
- Scanner di codici a barre con mobile_scannerFatto
- Open Food Facts, endpoint italiano e mondialeFatto
- Database SQLite locale con CRUD completoFatto
- Notifiche locali, immediate e riepilogo quotidianoFatto
- Lista della spesa condivisibileFatto
- Filtro per luogo: frigo, dispensa, freezerFatto
- Codici duplicati e inserimento manualeFatto
- Oltre 100 categorie di prodotti italianiFatto
- Dalla lista della spesa direttamente in dispensaFatto
- Storico prezzi con minimo, massimo e mediaFatto
- Widget iOS con le prossime scadenzeProssimo
- Statistiche sugli sprechi: cibo scaduto per meseProssimo
- Ricette suggerite con i prodotti in scadenzaProssimo
- Uscita su App StoreProssimo
Dietro le quinte
Scadoo è un progetto personale: l'ho progettato e scritto da solo in Flutter, dall'architettura all'ultima animazione della scheda prodotto.