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.