leafittome/docs/architektur.md
cschlaefke f0b420b656 V3: Stellplatz-Bewertung (Standort-Analyse per Foto + Eignungs-Sterne auf Abruf)
Letzter Roadmap-Punkt von V3. Foto-basierte Lichtverhältnis-Analyse pro
Stellplatz (Cloud Function analyzeLocation) und darauf aufbauende, rein
textbasierte Eignungs-Bewertung einer Pflanze (assessPlantFit) — beides
ausschließlich auf Abruf, nicht automatisch (Kostenkontrolle, eigener
Anthropic-Key). Neue Stellplatz-Detailseite, Verlinkung im Pflanzen-Detail,
Erkennung veralteter Bewertungen bei Umzug/Neuanalyse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:17:18 +02:00

7.7 KiB

Architektur & Projektstruktur

Dieses Dokument erklärt den Aufbau des LeafItToMe-Codes — auch als Blaupause für weitere Flutter-Apps.

Ordnerstruktur

lib/
├── main.dart                  # Einstiegspunkt: lädt SharedPreferences, startet die App
├── app.dart                   # MaterialApp: Theme, Routing, Lokalisierung, Schriftgröße
├── core/                      # Alles, was NICHT zu einem einzelnen Feature gehört
│   ├── router/app_router.dart # Zentrale Routen-Definition (go_router)
│   ├── settings/              # App-Einstellungen (Theme, Schriftgröße, Erinnerungszeit)
│   ├── theme/app_theme.dart   # Farbschema und Bedienbarkeits-Anpassungen
│   └── widgets/app_drawer.dart# Das Hamburger-Menü
├── features/                  # Ein Ordner pro Fachlichkeit
│   ├── auth/                  # Login/Registrierung + Haushalts-Bootstrap
│   ├── today/                 # Tages-Checkliste (Start-Screen)
│   ├── plants/                # Pflanzen: Liste, Profil, Anlegen/Bearbeiten
│   ├── locations/             # Stellplätze
│   ├── household/             # Haushalt: Mitglieder, Einladen, Wechseln
│   └── settings/              # Einstellungs-Screen
└── l10n/
    ├── app_de.arb             # ALLE deutschen Texte (Quelle)
    └── generated/             # Automatisch generierter Code (nicht anfassen)

Schichten innerhalb eines Features

Jedes Feature folgt demselben Muster (nicht jedes braucht alle Schichten):

  • domain/ — Die fachlichen Modelle (Plant, DueTask, PlantLocation). Reines Dart, keine Flutter- oder Firebase-Abhängigkeiten. Hier steckt die Fachlogik (z. B. „Fälligkeit = letzte Erledigung + Intervall").
  • data/ — Datenhaltung: StreamProvider liefern die Live-Daten aus Firestore (Echtzeit-Sync im Haushalt), Repository-Klassen kapseln die Schreibzugriffe (anlegen, ändern, bestätigen). In Tests werden Auth und Firestore per Provider-Override durch Mocks ersetzt (firebase_auth_mocks, fake_cloud_firestore).
  • application/ — Abgeleitete Zustände / Anwendungslogik, z. B. dueTasksProvider, der aus den Pflanzen die heute fälligen Aufgaben berechnet.
  • presentation/ — Die Screens und Widgets.

Faustregel für Abhängigkeiten: presentation → application → data → domain. Niemals andersherum, und Features greifen nur über Provider aufeinander zu.

State Management: Riverpod in 3 Sätzen

  1. Ein Notifier hält veränderlichen Zustand (z. B. die Pflanzenliste) und bietet Methoden zum Ändern (addPlant, confirmTask); ein Provider berechnet abgeleiteten Zustand (z. B. fällige Aufgaben) und aktualisiert sich automatisch, wenn sich seine Quellen ändern.
  2. Screens sind ConsumerWidgets: ref.watch(provider) liest den Zustand und baut die UI bei Änderungen neu; ref.read(provider.notifier).methode() löst Änderungen aus (in Callbacks).
  3. ProviderScope in main.dart ist die Wurzel; dort werden auch echte Abhängigkeiten (SharedPreferences, später Firebase) hineingereicht — Tests können sie durch Mocks ersetzen.

Kernentscheidungen im fachlichen Modell

  • Aufgaben werden nicht gespeichert, sondern berechnet. Es gibt keine „Task-Tabelle": Aus lastWatered + wateringIntervalDays ergibt sich die Fälligkeit. Eine Bestätigung setzt nur lastWatered neu — dadurch kann nichts inkonsistent werden, und Überfälliges „wandert" automatisch mit. Seit V3 gibt es zusätzlich Umtopfen als dritten Aufgabentyp — als Einziger opt-in (repottingIntervalMonths ist optional; nur wer es setzt, bekommt Erinnerungen) und mit Intervall in Kalender-Monaten statt Tagen (ein 31. wird auf den Monatsletzten gekürzt; Logik gespiegelt in App due_tasks_provider und Function reminders.ts).
  • Haushaltszentriert gedacht: Pflanzen, Stellplätze und Aufgaben gehören dem Haushalt, nicht einem Nutzer. In Firestore wird das households/{id}/plants/{id} usw. — so ist V2 (Sharing) nur „weiteres Mitglied hinzufügen", keine Datenmigration.
  • Mehrere Haushalte pro Nutzer: Quelle der Wahrheit für die Mitgliedschaft ist das Feld memberUids der Haushalte (myHouseholdsProvider fragt sie per arrayContains ab). users/{uid}.householdId ist nur noch der Zeiger auf den gerade aktiven Haushalt — Wechseln heißt: diesen Zeiger umstellen (Haushalts-Screen „Meine Haushalte“ oder Wechsler im Menü). Der aktive Haushalt bestimmt Pflanzen-/Stellplatz-Listen und wohin neue Pflanzen wandern; die Tages-Checkliste und die Sammel-Push aggregieren dagegen über alle Haushalte (Checkliste mit Zwischenüberschriften, DueTask trägt dafür seine Haushalts-Herkunft). Jeder Haushalt hat einen Besitzer (ownerUid): nur er entfernt Mitglieder (removeMember-Function), er kann nicht austreten; alle anderen können per leaveHousehold-Function austreten. Umbenennen dürfen volle Mitglieder direkt (Rules lassen am Haushalts-Dokument clientseitig nur noch name zu).
  • null bei lastWatered heißt „noch nie" → Aufgabe ist sofort fällig. So braucht eine neu angelegte Pflanze keine Sonderbehandlung.
  • Stellplatz-Bewertung (V3) ist zweistufig und beides auf Abruf, nicht automatisch: (1) Eine Standort-Analyse pro Stellplatz (Foto → Cloud Function analyzeLocation → Claude schätzt Lichtverhältnisse ein, eine von vier festen Kategorien + Freitext, gespeichert direkt auf PlantLocation, keine Historie — immer der aktuelle Stand). (2) Eignungs-Sterne pro Pflanze für ihren aktuell zugeordneten Stellplatz (Cloud Function assessPlantFit, rein textbasiert anhand Pflegeprofil + vorhandener Standort-Analyse, kein neues Foto nötig), gespeichert direkt auf Plant (fitStars, fitReasoning, fitLocationId, fitAssessedAt). Beide Schritte lösen ausschließlich Buttons aus — bewusst keine automatische Neuberechnung bei Änderungen, da der Nutzer die Claude-Aufrufe über seinen eigenen API-Key bezahlt. Ein Fit-Ergebnis gilt als veraltet (UI zeigt Hinweis statt Sterne), wenn die Pflanze seither an einen anderen Stellplatz verschoben wurde oder der Stellplatz seither neu analysiert wurde — rein clientseitig durch Vergleich der Zeitstempel/IDs berechnet, kein weiteres Feld nötig.
  • Haushalts-Bootstrap läuft reaktiv, nicht am Login-Aufruf: Beim ersten Login (egal ob E-Mail-Registrierung, Apple oder künftig Google) müssen Profil-Dokument (users/{uid}) und ein eigener Haushalt angelegt werden — ohne sie lehnen die Rules jeden Schreibzugriff ab. Statt den Bootstrap an die einzelnen Login-Methoden zu hängen (fehleranfällig: der Router leitet schon bei „angemeldet“ weiter, ein Bootstrap-Fehler danach wäre unsichtbar), beobachtet householdBootstrapProvider (auth/data) das users-Dokument und legt beides an, sobald „angemeldet, aber kein Profil“ eintritt. Das BootstrapGate in app.dart zeigt so lange einen Warte-Bildschirm und macht Fehler mit „Erneut versuchen“ sichtbar.

Übersetzungen (i18n)

Alle Texte stehen in lib/l10n/app_de.arb als Schlüssel-Wert-Paare. Im Code heißt es nie Text('Gießen'), sondern Text(l10n.taskWaterTitle(name)). Englisch später hinzufügen = eine Datei app_en.arb mit denselben Schlüsseln anlegen und flutter gen-l10n ausführen — kein Code-Umbau.

Nach jeder Änderung an der .arb-Datei: flutter gen-l10n ausführen (passiert bei flutter run auch automatisch).

Bedienbarkeit (Zielgruppe: auch ältere Menschen)

  • Schriftgröße in den Einstellungen wirkt zusätzlich zur System-Schriftgröße (app.dart, builder).
  • „Hoher Kontrast" nutzt Material 3 contrastLevel — dasselbe Farbschema, kräftigere Kontraste.
  • Große Touch-Ziele (app_theme.dart), keine Swipe-Gesten, alle Funktionen über sichtbare Buttons und das Hamburger-Menü.