Teil 5: Kurven zeichnen – Temperaturverläufe mit Diagrammen visualisieren

Hallo und herzlich willkommen zurück!

In Teil 4 haben wir unserer Wetter-App eine wichtige Funktion spendiert: die Adresssuche. Nun können wir das aktuelle Wetter nicht nur für unseren GPS-Standort, sondern für jeden beliebigen Ort abfragen. Das ist schon ziemlich cool!

Aber oft sagt ein Bild mehr als tausend Worte – oder in unserem Fall, eine einzelne Temperaturzahl. Wie hat sich die Temperatur entwickelt? Was erwartet uns in den nächsten Tagen? Um diese Fragen zu beantworten, werden wir heute ein Liniendiagramm in unsere App integrieren, das den Temperaturverlauf der letzten 7 Tage und eine Prognose für die nächsten 7 Tage anzeigt.

Das Ziel für heute:

  • Wir untersuchen den Code für Teil 5 aus unserem Repository.
  • Wir lernen das fl_chart-Paket kennen, ein mächtiges Werkzeug zur Erstellung von Diagrammen in Flutter.
  • Wir passen unsere Datenabfrage an, um auch stündliche Temperaturdaten von Open-Meteo zu erhalten.
  • Wir sehen, wie diese Daten in eine für das Diagramm geeignete Form gebracht werden.
  • Wir bauen ein neues Widget (TemperatureChart), das die Daten visualisiert, inklusive Achsenbeschriftung, geglätteter Linie und interaktiven Tooltips.

Warum Diagramme?

Diagramme sind ein hervorragendes Mittel, um Trends und Muster in Daten schnell erfassbar zu machen. Ein Blick auf die Temperaturkurve gibt uns ein viel besseres Gefühl für das Wetter als nur die aktuelle Zahl. Für unser Lernprojekt ist es außerdem eine tolle Gelegenheit, uns mit Datenvisualisierung in Flutter auseinanderzusetzen.

Schritt 1: Den Code für Teil 5 holen

Der Code für diesen Beitrag ist wie gewohnt im Git-Repository vorbereitet.

  1. Öffne ein Terminal in deinem Projektordner (flutter_weather_app_blog).
  2. Stelle sicher, dass du keine ungespeicherten Änderungen hast.
  3. Wechsle zum main-Branch und aktualisiere:
    git checkout main
    git pull origin main
  4. Checke den Code-Stand für Teil 5 aus: (Tag-Name ggf. anpassen)
    git checkout part5-temperature-chart
    (Denke an die ‚detached HEAD‘-Meldung.)
  5. Ganz wichtig: Abhängigkeiten holen & Code generieren:
    flutter pub get
    dart run build_runner build --delete-conflicting-outputs
    (Wir haben das fl_chart-Paket hinzugefügt und einige Datenmodelle erweitert.)

Öffne das Projekt nun in VS Code. Auf den ersten Blick sieht die App vielleicht noch nicht viel anders aus, aber unter der Haube hat sich einiges getan, um die Diagrammdarstellung vorzubereiten und zu implementieren.

Schritt 2: Die neue Werkzeugkiste – Das fl_chart-Paket

Um Diagramme zu zeichnen, ohne das Rad neu erfinden zu müssen, verwenden wir ein beliebtes Flutter-Paket namens fl_chart.

  • pubspec.yaml: In dieser Datei findest du unter dependencies: den neuen Eintrag:
    dependencies:
    # ... andere Pakete ...
    fl_chart: ^0.68.0 # Version prüfen

    fl_chart ist sehr vielseitig und unterstützt verschiedene Diagrammtypen (Linien-, Balken-, Kreisdiagramme etc.). Wir konzentrieren uns auf Liniendiagramme.

Schritt 3: Mehr Daten von der API – Stündliche Temperaturen

Unser Diagramm soll einen Verlauf über 14 Tage zeigen (7 Vergangenheit, 7 Zukunft). Dafür brauchen wir detailliertere Daten als nur die aktuelle Temperatur.

  • WeatherApiService (lib/src/features/weather/data/datasources/weather_api_service.dart):
    • Die Methode getCurrentWeather wurde umbenannt zu getForecastWeather, da sie jetzt mehr als nur die aktuellen Daten liefert.
    • Entscheidende Änderung in den queryParameters:
      // Ausschnitt aus getForecastWeather in WeatherApiService
      final queryParameters = {
      // ... latitude, longitude, current_weather ...
      'hourly': 'temperature_2m', // NEU: Fordert stündliche Temperaturdaten an
      'past_days': pastDays.toString(), // NEU: z.B. 7
      'forecast_days': forecastDays.toString(), // NEU: z.B. 7
      // ... timezone ...
      };

      Wir fordern jetzt explizit hourly=temperature_2m an und spezifizieren über past_days und forecast_days den gewünschten Zeitraum. Open-Meteo liefert uns dann eine lange Liste von Zeitstempeln und den dazugehörigen Temperaturen im 2-Meter-Höhenintervall.

Schritt 4: Die neuen Datenstrukturen (data Layer Models)

Die API liefert die stündlichen Daten in einer spezifischen Struktur. Wir brauchen neue Dart-Klassen (Models), um diese abzubilden:

  • lib/src/features/weather/data/models/hourly_units_model.dart (NEU):
    • Open-Meteo sendet ein separates Objekt hourly_units, das die Einheiten für die stündlichen Werte angibt (z.B. "time": "iso8601", "temperature_2m": "°C"). Diese kleine Klasse bildet das ab.
  • lib/src/features/weather/data/models/hourly_data_model.dart (NEU):
    • Das ist das Herzstück der neuen API-Daten. Es bildet das hourly-Objekt aus der API-Antwort ab, das typischerweise zwei Listen enthält:
      • time: Eine Liste von Zeitstempel-Strings (z.B. "2023-10-27T10:00").
      • temperature_2m: Eine Liste von Temperaturwerten (Zahlen).
    • Die fromJson-Methode in dieser Klasse ist wichtig: Sie nimmt die rohen Listen aus dem JSON, parst die Zeitstempel-Strings mit unserem DateFormatter.tryParseApiDateTime in DateTime-Objekte und wandelt die Temperaturwerte in double um. Sie stellt auch sicher, dass beide Listen die gleiche Länge haben und behandelt fehlerhafte oder fehlende Werte (z.B. indem sie double.nan für ungültige Temperaturen verwendet).
  • lib/src/features/weather/data/models/forecast_response_model.dart (GEÄNDERT):
    • Unser Haupt-Antwortmodell wurde erweitert, um die neuen HourlyUnitsModel und HourlyDataModel aufzunehmen:
      // Ausschnitt aus ForecastResponseModel
      class ForecastResponseModel {
      // ... bestehende Felder ...
      final CurrentWeatherModel? currentWeather;
      final HourlyUnitsModel? hourlyUnits; // NEU
      final HourlyDataModel? hourly; // NEU

      // Konstruktor und toJson angepasst...

      factory ForecastResponseModel.fromJson(Map json) {
      // ... bestehendes Parsing ...
      final units = json.containsKey('hourly_units') /* ... */ ? HourlyUnitsModel.fromJson(json['hourly_units']) : null;
      final data = json.containsKey('hourly') /* ... */ ? HourlyDataModel.fromJson(json['hourly']) : null;
      return ForecastResponseModel(
      // ...
      hourlyUnits: units,
      hourly: data,
      );
      }
      }

Schritt 5: Die App-internen Daten (domain Layer Entities)

Unsere App-Logik und UI sollten nicht direkt mit den API-spezifischen Models arbeiten. Wir brauchen eine saubere, app-interne Repräsentation der Daten.

  • lib/src/features/weather/domain/entities/chart_point.dart (NEU):
    • Eine sehr einfache Klasse, die einen einzelnen Punkt im Diagramm repräsentiert:
      class ChartPoint extends Equatable {
      final DateTime time; // X-Wert
      final double temperature; // Y-Wert
      // ... Konstruktor, props ...
      }

  • lib/src/features/weather/domain/entities/weather_data.dart (GEÄNDERT/ERSETZT):
    • Diese Entität, die bisher nur CurrentWeatherData hieß und nur die aktuelle Temperatur enthielt, wurde nun zur zentralen WeatherData-Klasse.
    • Sie enthält jetzt zusätzlich eine Liste von ChartPoint-Objekten für den Temperaturverlauf:
      // Ausschnitt aus WeatherData
      class WeatherData extends Equatable {
      final double currentTemperature;
      final DateTime? lastUpdatedTime;
      final List hourlyForecast; // NEU

      // Konstruktor, props, empty, copyWith angepasst...
      }

    • (Die alte current_weather_data.dart kann gelöscht werden, wenn sie nicht mehr referenziert wird.)

Schritt 6: Datenaufbereitung im Repository (data Layer)

Das WeatherRepositoryImpl (lib/src/features/weather/data/repositories/weather_repository_impl.dart) ist dafür zuständig, die Rohdaten vom WeatherApiService zu holen und sie in unsere App-internen WeatherData-Entität umzuwandeln.

  • getWeatherForLocation-Methode (GEÄNDERT):
    • Ruft jetzt _apiService.getForecastWeather() auf (die umbenannte Methode, die auch stündliche Daten holt).
    • Nachdem die aktuelle Temperatur extrahiert wurde, iteriert es durch die time– und temperature_2m-Listen aus dem forecastResponse.hourly-Objekt.
    • Für jedes gültige Zeit/Temperatur-Paar wird ein ChartPoint-Objekt erstellt und der hourlyPoints-Liste hinzugefügt. Ungültige Temperaturen (NaN) werden übersprungen.
    • Am Ende wird das WeatherData-Objekt mit currentTemperature, lastUpdatedTime und der hourlyForecast-Liste erstellt und zurückgegeben.
    • Das Interface WeatherRepository (domain/repositories/weather_repository.dart) wurde natürlich angepasst, sodass getWeatherForLocation jetzt Future<Either> zurückgibt.

Schritt 7: State Management (application Layer)

Die Änderungen im WeatherState und WeatherNotifier sind minimal, da unsere Architektur gut vorbereitet war.

  • lib/src/features/weather/application/weather_state.dart (GEÄNDERT):
    • Das Feld currentWeatherData wurde durch weatherData (vom Typ WeatherData, unserer neuen, umfassenderen Entität) ersetzt.
    • Die initial() und copyWith() Methoden wurden entsprechend angepasst.
  • lib/src/features/weather/application/weather_notifier.dart (MINIMAL GEÄNDERT):
    • Die Methode _fetchWeatherDataAndUpdateState nimmt nun WeatherData vom Repository entgegen und speichert es im weatherData-Feld des WeatherState. Die Logik an sich bleibt gleich, da das Repository die Hauptarbeit der Datenumwandlung übernimmt.

Schritt 8: Das Diagramm-Widget (presentation Layer)

Jetzt kommt der spannende Teil – die Visualisierung!

  • lib/src/features/weather/presentation/widgets/temperature_chart.dart (NEU):

    • Dies ist ein neues StatelessWidget, das die chartData (eine List) als Parameter erwartet.
    • Kernstück: LineChart von fl_chart:
      • LineChartData: Konfiguriert das gesamte Diagramm.
      • lineBarsData: Definiert die Linien. Wir haben eine LineChartBarData.
        • spots: Hier werden unsere ChartPoint-Objekte in FlSpot-Objekte umgewandelt, die fl_chart versteht (FlSpot(zeit_als_double, temperatur)).
        • isCurved: true: Macht die Linie schön weich.
        • color, barWidth: Aussehen der Linie.
        • dotData: FlDotData(show: false): Wir zeigen keine einzelnen Punkte auf der Linie an.
        • belowBarData: Füllt den Bereich unter der Linie mit einem Farbverlauf (optional, aber schick).
      • titlesData: Konfiguriert die Achsenbeschriftungen.
        • bottomTitles: Für die X-Achse (Zeit). getTitlesWidget ist eine Funktion, die für jeden Achsenpunkt ein Widget zurückgibt. Wir formatieren hier den Zeitstempel mit unserem DateFormatter.formatChartAxisDay (z.B. „Mo 15.07.“) und zeigen nur alle paar Tage ein Label, um Überlappung zu vermeiden. Die SideTitleWidget(meta: meta, ...) Konstruktion ist hier wichtig.
        • leftTitles: Für die Y-Achse (Temperatur). Zeigt Temperaturwerte in sinnvollen Intervallen (z.B. alle 5 Grad).
      • gridData: Zeichnet das Hintergrundgitter.
      • borderData: Zeichnet einen Rahmen um das Diagramm.
      • lineTouchData: Ermöglicht Interaktion.
        • touchTooltipData: Konfiguriert die Tooltips, die erscheinen, wenn man auf die Linie tippt. getTooltipItems erstellt den Inhalt des Tooltips (Datum, Uhrzeit, Temperatur). getTooltipColor (früher tooltipBgColor) setzt die Hintergrundfarbe des Tooltips.
      • minY, maxY: Bestimmen den sichtbaren Bereich der Y-Achse. Wir berechnen diese dynamisch aus den Daten und fügen etwas Puffer hinzu.
    • Deutsche Tageskürzel: In lib/main.dart haben wir initializeDateFormatting('de_DE', null); hinzugefügt. Dadurch verwendet DateFormat im DateFormatter standardmäßig deutsche Formate, also auch „Mo, Di, Mi…“ für die Tageskürzel in der X-Achsen-Beschriftung.
  • lib/src/features/weather/presentation/screens/weather_screen.dart (GEÄNDERT):

    • _buildSuccessContent (NEUE Hilfsmethode): Um den _buildContent-Switch übersichtlich zu halten, wurde der Code, der bei WeatherStatus.success angezeigt wird, in diese neue Methode ausgelagert.
    • Innerhalb von _buildSuccessContent wird nun zusätzlich zum CurrentTemperatureDisplay auch das TemperatureChart-Widget hinzugefügt. Es bekommt data.hourlyForecast (wobei data hier das WeatherData-Objekt aus dem State ist) übergeben.
    • Die gesamte Erfolgsansicht ist in eine ListView gewickelt, damit der Inhalt scrollbar wird, falls er nicht auf den Bildschirm passt (was mit dem Diagramm nun der Fall sein kann).

Schritt 9: Ausführen und Bestaunen!

Starte die App (F5):

  1. Lasse das Wetter für deinen aktuellen Standort oder einen gesuchten Ort laden.
  2. Unter der aktuellen Temperatur solltest du nun ein Liniendiagramm sehen!
  3. Interaktion: Tippe auf verschiedene Punkte der Linie. Ein Tooltip sollte mit dem genauen Datum, der Uhrzeit und der Temperatur für diesen Punkt erscheinen.
  4. Verlauf: Die Linie sollte die Temperatur der letzten 7 Tage (Vergangenheit) und der nächsten 7 Tage (Prognose) darstellen.
  5. Achsen: Die X-Achse sollte Tage anzeigen (z.B. „Heute“, „Morgen“, „Mi 17.07.“), die Y-Achse die Temperaturskala.

Was haben wir gelernt?

  • Wie man ein externes Paket (fl_chart) für komplexe UI-Elemente einbindet.
  • Wie man API-Anfragen anpasst, um mehr Daten (stündliche Werte) zu erhalten.
  • Wie man Datenmodelle und Entitäten erweitert, um neue Informationen zu speichern.
  • Wie man Rohdaten von einer API für die Darstellung in einem Diagramm aufbereitet (Mapping zu ChartPoint und FlSpot).
  • Die Grundlagen der Konfiguration eines LineChart mit fl_chart, inklusive:
    • Datenpunkte (spots)
    • Aussehen der Linie (LineChartBarData)
    • Achsenbeschriftung (titlesData, SideTitleWidget)
    • Gitternetz (gridData)
    • Interaktive Tooltips (lineTouchData)
  • Wie man die UI strukturert (ListView), um auch größere Inhalte scrollbar zu machen.
  • Die Wichtigkeit der deutschen Locale-Initialisierung für korrekte Datumsformate.

Ausblick auf Teil 6:

Unsere App wird immer funktionaler und sieht auch noch besser aus! Im nächsten und vorerst letzten Teil dieser Kernserie implementieren wir unsere Spezialfunktion: die Berechnung und Anzeige der Grünlandtemperatursumme (GTS). Dafür müssen wir historische Tagesmittelwerte von einer anderen Open-Meteo API abrufen und eine spezifische Berechnungslogik umsetzen.

Das wird noch einmal spannend und zeigt, wie man mit Flutter auch komplexere fachliche Anforderungen umsetzen kann. Bleib dran!


Weiterführende Ressourcen & Vertiefung

In diesem Teil haben wir einige neue Konzepte und Werkzeuge kennengelernt. Hier sind Links, falls du tiefer eintauchen möchtest:

  1. fl_chart Paket:
  2. Datenmodellierung & JSON Parsing in Dart:
  3. Datum & Zeit Formatierung (intl Paket):
  4. Flutter State Management (Riverpod):
    • Offizielle Riverpod Dokumentation: https://riverpod.dev/ – Die beste Quelle, um Riverpod von Grund auf zu lernen oder spezifische Konzepte wie StateNotifierProvider, ref.watch und ref.read nachzuschlagen.
  5. Flutter Layout Grundlagen (ListView, Column, SizedBox):