Teil 3: Der erste Meilenstein: Aktuelle Temperatur am aktuellen Ort – Den Code verstehen
Hallo und willkommen zurück zur Serie!
In Teil 2 haben wir unsere Werkzeuge geschärft und die Flutter-Entwicklungsumgebung auf deinem Windows-PC eingerichtet. Wir haben sogar die Standard-Flutter-App zum Laufen gebracht. Super!
Heute tauchen wir endlich in den eigentlichen Code unserer Wetter-App ein. Wir überspringen das manuelle Tippen jedes Zeichens und konzentrieren uns stattdessen darauf, den Code für unseren ersten Meilenstein zu verstehen: Die App soll die aktuelle Temperatur für deinen aktuellen GPS-Standort anzeigen.
Das Ziel für heute:
- Wir schauen uns den vorbereiteten Code für Teil 3 an (den du aus einem Repository herunterladen kannst).
- Wir zerlegen die Projektstruktur und verstehen, welche Datei wo hingehört und warum.
- Wir verfolgen den Datenfluss: Wie kommt die Temperatur vom Internet auf deinen Bildschirm?
- Wir beleuchten die wichtigsten Konzepte: Architektur, State Management mit Riverpod, Fehlerbehandlung und mehr.
Warum dieser Ansatz?
Eine App zu bauen ist wie ein Haus zu bauen. Man könnte einfach loslegen, aber ein guter Architekt plant zuerst. Wir haben den Code für diesen ersten Schritt bereits nach einem bewährten Plan (einer „sauberen Architektur“) strukturiert. Indem wir diesen fertigen, aber einfachen Code untersuchen, lernst du nicht nur, wie man eine Funktion implementiert, sondern auch, wie man Code organisiert, damit er später leicht erweitert, getestet und gewartet werden kann. Das ist entscheidend, um selbst gute Apps zu schreiben!
Schritt 1: Den Code holen
Der gesamte Code für diesen Teil der Serie ist in einem Git-Repository vorbereitet.
- Repository klonen (falls noch nicht geschehen):
 Öffne eine Eingabeaufforderung oder ein Terminal und navigiere zu dem Ordner, in dem du deine Projekte speichern möchtest (z.B.C:\dev\flutter_projekte\). Führe dann folgenden Befehl aus:git clone https://github.com/hschewe/flutter_weather_app_blog.git
 Wechsle in das neu erstellte Verzeichnis:bash cd flutter_weather_app_blog
- Den richtigen Stand auschecken: Für jeden Teil der Serie gibt es einen Git-Tag. Um den Code-Stand für Teil 3 zu bekommen, führe aus:git checkout part3-current-temp-gps.
 (Git meldet möglicherweise, dass du dich in einem ‚detached HEAD‘-Zustand befindest. Das ist normal und bedeutet, du schaust dir einen spezifischen Punkt in der Vergangenheit an. Du kannst den den Code untersuchen, ausführen und sogar temporäre Änderungen vornehmen. Wenn du zur normalen Entwicklung zurückkehren willst, kannst du einfach wieder den main-Branch auschecken (git checkout main)).
- Projekt in VS Code öffnen: Öffne VS Code und wähle File > Open Folder...und navigiere zumflutter_weather_app_blog-Ordner.
- Abhängigkeiten installieren: Öffne ein Terminal in VS Code (Terminal > New Terminal) und führe aus:flutter pub get
- Code generieren: Da wir Riverpod mit Code-Generierung verwenden, müssen wir diesen Schritt ausführen:dart run build_runner build --delete-conflicting-outputs
 Dieser Befehl liest spezielle Anmerkungen (@riverpod) im Code und erstellt automatisch benötigte Hilfsdateien (die auf.g.dartenden).
Jetzt ist der Code bereit zur Untersuchung!
Schritt 2: Der große Überblick – Die Architektur
Bevor wir in einzelne Dateien schauen, betrachten wir den Bauplan. Unsere App folgt einer Schichtenarchitektur, inspiriert von „Clean Architecture“. Stell dir vor, wir bauen Schichten wie bei einer Zwiebel:
+-------------------------------------------------+
| Presentation (UI) Layer                         | <--- Das, was der Nutzer sieht (Widgets, Screens)
|    - Widgets                                    |      Interagiert mit dem Application Layer
|    - Screens                                    |
|    - State Management (Notifier/Provider)       |
+-------------------------------------------------+
      ^                                      | Dependency Rule
      | Calls                                | (Innere Schichten kennen Äußere nicht)
+-------------------------------------------------+
| Application / Domain Layer                      | <--- Die Logik der App
|    - State Notifier (Logik-Orchestrierung)      |      Definiert, WAS die App tut
|    - Repository Interface (Datenvertrag)        |      Kennt nur Entities
|    - Entities (App-Datenstrukturen)             |
+-------------------------------------------------+
      ^                                      |
      | Implements / Calls                   |
+-------------------------------------------------+
| Data Layer                                      | <--- Datenbeschaffung & -speicherung
|    - Repository Implementation                  |      Implementiert den Vertrag
|    - Data Sources (API, GPS, DB)                |      Spricht mit der Außenwelt
|    - Models (API/DB-Datenstrukturen)            |
+-------------------------------------------------+
      ^ Depends on                             ^ Depends on
      |                                        |
+-------------------------------------------------+
| Core Layer                                      | <--- App-übergreifende Helfer
|    - Utils (Logger, Formatter)                  |      Von allen Schichten nutzbar
|    - Error Handling                             |
|    - Networking Client                          |
+-------------------------------------------------+
- Core: Enthält grundlegende Helferlein, die überall gebraucht werden könnten (wie unser Logger).
- Data: Kümmert sich darum, woher die Daten kommen (API, GPS) und wie sie technisch abgefragt werden. Kennt die genaue Struktur der externen Daten.
- Domain/Application: Das Herzstück. Definiert, welche Daten die App braucht (Entities) und welche Operationen möglich sind (Repository Interface), aber nicht, wie sie beschafft werden. Hier sitzt auch die Logik, die auf Nutzeraktionen reagiert (Notifier). Diese Schicht sollte unabhängig von UI-Details oder spezifischen Datenbanken/APIs sein.
- Presentation (UI): Zeigt die Daten an (Screens,Widgets) und nimmt Nutzereingaben entgegen. Sie spricht nur mit dem Application Layer (über den Notifier), um Daten zu bekommen oder Aktionen auszulösen.
Die goldene Regel: Abhängigkeiten zeigen immer nach innen! Die UI kennt die Application/Domain Layer, aber nicht die Data Layer. Die Domain Layer kennt niemanden außerhalb (außer Core). Das macht das System flexibel und testbar.
Schritt 3: Ein Rundgang durch die Ordner (lib/src/)
Schauen wir uns an, wie diese Architektur in unserer Ordnerstruktur abgebildet ist:
- lib/main.dart: Der allererste Startpunkt. Initialisiert das Logging,- WidgetsFlutterBinding(wichtig für Plugins) und startet die App innerhalb einer- ProviderScope(notwendig für Riverpod).
- lib/app.dart: Enthält das- MaterialApp-Widget, das die grundlegende Struktur, das Theme (Aussehen) und die Startseite (- WeatherScreen) unserer App definiert.
- lib/src/core/: Unser Fundament.- error/: Enthält- exceptions.dart(spezifische technische Fehler wie- NetworkException) und- failure.dart(abstraktere Fehler wie- NetworkFailure, die die Logik verstehen kann). Diese Trennung hilft, Fehler sauber zu behandeln.
- location/:- location_service.dartkapselt die Interaktion mit dem- geolocator-Paket. Alle GPS- und Berechtigungs-Anfragen laufen hierüber. Wird über Riverpod bereitgestellt (- locationServiceProvider).
- networking/:- http_client.dartstellt eine globale Instanz des- http.Clientbereit (über- httpClientProvider). Das macht es einfach, ihn in Tests durch einen Mock zu ersetzen.
- utils/: Enthält Helfer wie- logger.dart(für strukturierte Logs) und- date_formatter.dart(um z.B. die Uhrzeit anzuzeigen).
- constants/:- app_constants.dartsammelt zentrale Konstanten (hier erstmal nur- myLocationLabel).
 
- lib/src/features/weather/: Alles, was mit der Wetterfunktion zu tun hat.- data/: Datenbeschaffung.- models/:- current_weather_model.dart,- forecast_response_model.dart. Diese Dart-Klassen spiegeln exakt die Struktur des JSON wider, das wir von der Open-Meteo API bekommen. Sie enthalten- fromJson-Methoden, um JSON in Dart-Objekte umzuwandeln.
- datasources/:- weather_api_service.dart. Spricht über den- http.Clientmit der Open-Meteo API (- getCurrentWeather-Methode). Parst die JSON-Antwort mithilfe der- Modelsund wirft spezifische- Exceptions(- ApiException,- NetworkException,- DataParsingException). Wird über Riverpod bereitgestellt (- weatherApiServiceProvider).
- repositories/:- weather_repository_impl.dart. Die konkrete Implementierung unseres Datenvertrags. Diese Klasse kennt den- WeatherApiServiceund den- LocationService. Sie ruft deren Methoden auf, fängt deren- Exceptionsab und wandelt sie in- Failuresum (z.B. wird- NetworkExceptionzu- NetworkFailure). Sie wandelt auch die API-- Modelsin die App-internen- Entitiesum. Wird über Riverpod bereitgestellt (- weatherRepositoryProvider).
 
- domain/: Das Kernstück der App-Logik.- entities/:- location_info.dart,- current_weather_data.dart. Einfache Dart-Klassen, die die Daten repräsentieren, wie sie die App intern benötigt, unabhängig von der API. Nutzen- Equatablefür einfache Vergleiche.
- repositories/:- weather_repository.dart. Das ist nur ein „Interface“ (abstrakte Klasse). Es definiert, was man mit Wetterdaten tun können muss (z.B.- getWeatherForLocation,- getCurrentLocationCoordinates), aber nicht wie. Das ist der Vertrag, den die- WeatherRepositoryImplerfüllen muss.
 
- application/: Die Orchestrierung.- weather_state.dart: Definiert, wie der Zustand des Wetter-Features aussieht: ein- enum WeatherStatus(initial, loading, success, failure), die eigentlichen- CurrentWeatherData, der- selectedLocationund ein optionales- Failure-Objekt. Nutzt- Equatableund- copyWith.
- weather_notifier.dart: Die Steuerzentrale. Ein- StateNotifier, der den- WeatherStatehält und aktualisiert. Er bekommt das- WeatherRepositoryübergeben (Dependency Injection durch Riverpod). Enthält die Logik für- fetchWeatherForCurrentLocationund- refreshWeatherData. Er ruft Methoden im Repository auf, behandelt das- Either<Failure, Success>-Ergebnis und aktualisiert den- stateentsprechend.
 
- presentation/: Die Benutzeroberfläche.- providers/:- weather_providers.dart. Definiert den- weatherNotifierProvider, der den- WeatherNotifiererstellt und der UI zur Verfügung stellt.
- widgets/: Kleine, wiederverwendbare UI-Teile.- location_header.dartzeigt den Ortsnamen,- current_temperature_display.dartzeigt die Temperatur und Zeit. Sie bekommen ihre Daten von außen übergeben.
- screens/:- weather_screen.dart. Der Hauptbildschirm. Ein- ConsumerStatefulWidget, das über- ref.watch(weatherNotifierProvider)den aktuellen- WeatherStatebekommt. Basierend auf dem- statusim State, zeigt es entweder einen Ladeindikator, die Wetter-Widgets oder eine Fehlermeldung (- _buildErrorWidget). Es nutzt- ref.read(weatherNotifierProvider.notifier)um Aktionen im Notifier auszulösen (initiales Laden, Refresh). Der- RefreshIndicatorermöglicht Pull-to-Refresh. Der- AnimatedSwitchersorgt für weiche Übergänge.
 
 
Schritt 4: Den Datenfluss verstehen
Wie hängt das alles zusammen, wenn die App startet?
- Start (main.dart->app.dart->WeatherScreen): Die App wird initialisiert,ProviderScopewird erstellt.WeatherScreenwird angezeigt.
- Initial Load (WeatherScreen.initState): Der Screen merkt, dass er neu ist (initialState.status == WeatherStatus.initial) und triggert überref.read(weatherNotifierProvider.notifier).fetchWeatherForCurrentLocation()den Ladevorgang im Notifier.
- Loading State (WeatherNotifier): Der Notifier setzt sofort seinenstateaufstatus: WeatherStatus.loading.
- UI Reaction (WeatherScreen): Da der Screen viaref.watchauf den State hört, wird er neu gebaut und zeigt jetzt (im_buildContent) einen Ladeindikator an.
- Get Coordinates (WeatherNotifier->WeatherRepository->LocationService):- Notifier ruft _weatherRepository.getCurrentLocationCoordinates()auf.
- Das Repo (WeatherRepositoryImpl) ruft_locationService.getCurrentPosition()auf.
- Der LocationServiceinteragiert mit demgeolocator, fragt ggf. nach Berechtigungen und holt diePosition.
- Wenn erfolgreich, gibt der Service die Positionzurück. Wenn ein Fehler auftritt (z.B. Berechtigung verweigert), wirft er eineLocationException.
- Das Repo fängt die Exception, wandelt sie ggf. in eineFailure(PermissionFailure,LocationFailure) um und gibtLeft(failure)zurück. Bei Erfolg erstellt esLocationInfound gibtRight(locationInfo)zurück.
 
- Notifier ruft 
- Handle Coordinates Result (WeatherNotifier):- Der Notifier erhält das Either. BeiLeft(failure)setzt er denstateaufstatus: WeatherStatus.failuremit demfailure-Objekt -> Die UI zeigt die Fehlermeldung.
- Bei Right(locationInfo)geht es weiter. Der Notifier versucht noch, den Namen via_weatherRepository.getLocationDisplayNamezu holen (was in Teil 3 noch nicht viel tut) und ruft dann_fetchWeatherDataAndUpdateState(finalLocationInfo)auf.
 
- Der Notifier erhält das 
- Get Weather Data (WeatherNotifier->WeatherRepository->WeatherApiService):- Notifier ruft _weatherRepository.getWeatherForLocation(locationInfo)auf.
- Das Repo (WeatherRepositoryImpl) ruft_apiService.getCurrentWeather(...)auf.
- Der WeatherApiServicebaut die URL, macht denhttp.get-Aufruf, parst das JSON inForecastResponseModel, extrahiertCurrentWeatherModel. Bei Fehlern (Netzwerk, API-Status, Parsing) wirft erNetworkException,ApiException,DataParsingException.
- Das Repo fängt diese Exceptions, wandelt sie inFailures(NetworkFailure,ServerFailure) um und gibtLeft(failure)zurück. Bei Erfolg wandelt esCurrentWeatherModelinCurrentWeatherData(unser App-Entity) um und gibtRight(weatherData)zurück.
 
- Notifier ruft 
- Handle Weather Result (WeatherNotifier):- Der Notifier erhält das Either. BeiLeft(failure)setzt er denstateaufstatus: WeatherStatus.failuremit demfailure-Objekt -> Die UI zeigt die Fehlermeldung.
- Bei Right(weatherData)setzt er denstateaufstatus: WeatherStatus.success, speichertweatherDataundlocationInfoim State und löscht den Fehler (clearError: true).
 
- Der Notifier erhält das 
- UI Reaction (WeatherScreen): Der Screen hört wieder auf die State-Änderung (ref.watch). Er wird neu gebaut,_buildContenterkenntWeatherStatus.successund zeigt jetztLocationHeaderundCurrentTemperatureDisplaymit den Daten aus demstatean.
Schritt 5: Schlüsselkonzepte im Code
- Riverpod für State Management & Dependency Injection:
- ProviderScope(in- main.dart): Macht Provider global verfügbar.
- @riverpod/- ...Provider(z.B. in- location_service.dart,- weather_repository_impl.dart): Definiert, wie eine Instanz eines Service oder Repositories erstellt wird. Riverpod kümmert sich darum, dass nur eine Instanz erstellt und wiederverwendet wird. Das ist Dependency Injection: Komponenten bekommen ihre Abhängigkeiten (wie den- http.Clientoder den- LocationService) „injiziert“, statt sie selbst zu erstellen.
- StateNotifierProvider(- weather_providers.dart): Ein spezieller Provider für unseren- WeatherNotifier, der dessen Zustand (- WeatherState) verwaltet.
- ConsumerWidget/- ConsumerStatefulWidget(- WeatherScreen): Widgets, die auf Provider „hören“ können.
- ref.watch()(im- buildvon- WeatherScreen): Liest den Wert eines Providers und baut das Widget neu, wenn sich der Wert (hier der- WeatherState) ändert.
- ref.read()(im- initStateoder in Callbacks wie- onPressedvon- WeatherScreen): Liest den Wert eines Providers einmalig, ohne auf Änderungen zu hören. Wird verwendet, um Methoden im Notifier aufzurufen.
 
- Fehlerbehandlung (Either,Failure,Exception):- Services (LocationService,WeatherApiService) werfen spezifischeExceptionsbei technischen Problemen.
- Das Repository (WeatherRepositoryImpl) fängt dieseExceptionsund wandelt sie in allgemeinereFailuresum. Es gibt das Ergebnis alsEither<Failure, Success>zurück – ein klarer Weg, um Erfolg oder Misserfolg zu signalisieren, ohne Exceptions durch die ganze App zu werfen.
- Der Notifier (WeatherNotifier) behandelt dasEither-Ergebnis und aktualisiert denWeatherStateentsprechend (status: WeatherStatus.failureodersuccess).
- Die UI (WeatherScreen) reagiert auf denstatusund daserror-Feld im State und zeigt die passende Ansicht.
 
- Services (
- Asynchronität (Future,async,await): Netzwerk- und GPS-Anfragen dauern eine Weile.Futurerepräsentiert einen Wert, der irgendwann verfügbar sein wird.asyncmarkiert eine Funktion, dieawaitverwenden kann.awaitpausiert die Ausführung der Funktion, bis derFutureabgeschlossen ist, ohne die gesamte App zu blockieren. Wir sehen das intensiv im Notifier, Repository und den Services.
- Immutability & Equatable: Der WeatherStatewird nie direkt geändert. Stattdessen wird mitcopyWitheine neue Instanz mit den geänderten Werten erstellt. Das macht den Zustandsfluss vorhersagbar.Equatablehilft Riverpod (und uns beim Testen), effizient zu erkennen, ob sich der Zustand wirklich geändert hat, indem es Objekte anhand ihrer Eigenschaften vergleicht, nicht nur anhand ihrer Speicheradresse.

Schritt 6: Ausführen und Experimentieren!
Jetzt, wo du eine Vorstellung davon hast, wie der Code aufgebaut ist und funktioniert:
- Starte die App: Wähle dein Gerät in VS Code und drücke F5.
- Beobachte: Verfolge die Log-Ausgaben im „DEBUG CONSOLE“-Fenster von VS Code. Du solltest die Meldungen von AppLoggeraus den verschiedenen Schichten sehen.
- Experimentiere:
- Ändere Texte in den Widgets.
- Setze Haltepunkte (Breakpoints) in VS Code (klicke links neben die Zeilennummer) in verschiedenen Methoden (z.B. im Notifier, im Repo, im Service) und starte die App im Debug-Modus (F5). Steppe durch den Code (F10,F11), um den Fluss live zu sehen.
- Simuliere Fehler: Wirf testweise eine Exception im WeatherApiServiceoder gibLeft(NetworkFailure())im Repository zurück, um zu sehen, wie die Fehlerbehandlung in der UI greift.
 
Zusammenfassung und Nächste Schritte
Das war ein tiefer Einblick in den Code unseres ersten Meilensteins! Du hast gesehen, wie wir mit einer klaren Architektur und Werkzeugen wie Riverpod eine skalierbare und testbare Grundlage geschaffen haben, auch für eine zunächst einfache Funktion. Du verstehst jetzt (hoffentlich!) besser:
- Die Aufteilung in Schichten (Presentation, Domain/Application, Data, Core).
- Die Verantwortlichkeiten der einzelnen Komponenten (Widgets, Notifier, Repositories, Services).
- Den Daten- und Kontrollfluss durch die App.
- Die Grundprinzipien von State Management, Fehlerbehandlung und Asynchronität in Flutter.
Im nächsten Teil (Teil 4) bauen wir darauf auf und machen die App interaktiver: Wir fügen die Adresssuche hinzu!
Bleib dran und viel Spaß beim Erkunden des Codes!
Weiterführende Ressourcen & Vertiefung (für Teil 3)
Dieser Teil hat viele grundlegende Konzepte der Flutter-Entwicklung eingeführt. Hier sind Links zum Vertiefen:
- Projektstruktur & Architektur:
- Flutter App architecture samples (GitHub): https://github.com/flutter/samples/tree/main/experimental/context_menus (Beispielhaft, es gibt viele Ansätze) – Oft komplex, aber gibt Ideen. Unsere Struktur ist eine vereinfachte Form, die oft als „Feature-First“ mit Schichtentrennung bezeichnet wird.
- Clean Architecture in Flutter (Artikel/Videos): Suche nach „Clean Architecture Flutter“ für verschiedene Interpretationen und Erklärungen (z.B. von Reso Coder, Vandad Nahavandipoor).
 
- Flutter Pakete (Dependencies):
- Pakete verwenden (Offizielle Doku): https://docs.flutter.dev/packages-and-plugins/using-packages – Wie man Pakete in pubspec.yamlhinzufügt und nutzt.
- httpPaket: https://pub.dev/packages/http – Für Netzwerk-Anfragen.
- geolocatorPaket: https://pub.dev/packages/geolocator – Für GPS-Zugriff und Berechtigungen.
- loggingPaket: https://pub.dev/packages/logging – Für strukturiertes Logging.
- intlPaket: https://pub.dev/packages/intl – Für Internationalisierung und Formatierung (Datum, Zahlen).
- equatablePaket: https://pub.dev/packages/equatable – Vereinfacht das Überschreiben von- ==und- hashCodefür Wert-Vergleiche.
 
- Pakete verwenden (Offizielle Doku): https://docs.flutter.dev/packages-and-plugins/using-packages – Wie man Pakete in 
- State Management (Riverpod):
- Offizielle Riverpod Dokumentation: https://riverpod.dev/ – Die beste Quelle! Beginne mit „Getting Started“.
- Provider Typen (Übersicht): https://riverpod.dev/docs/concepts/providers – Erklärt Provider,StateNotifierProvideretc.
- ref.watchvs- ref.read: https://riverpod.dev/docs/concepts/reading_providers – Ein fundamentales Konzept in Riverpod.
- Code Generierung: https://riverpod.dev/docs/concepts/code_generation – Erklärung, wie @riverpodundbuild_runnerfunktionieren.
 
- Dart Grundlagen:
- Asynchrones Programmieren (Future,async,await): https://dart.dev/codelabs/async-await – Essentiell für Netzwerk- und Geräte-Interaktionen.
- Fehlerbehandlung (try-catch,Exceptions): https://dart.dev/guides/language/language-tour#exceptions
 
- Asynchrones Programmieren (
- Android Spezifika:
- App Manifest (AndroidManifest.xml): https://developer.android.com/guide/topics/manifest/manifest-intro – Grundlegende Informationen zur Manifest-Datei.
- Berechtigungen unter Android: https://developer.android.com/guide/topics/permissions/overview
 
- App Manifest (
 
					
0 Kommentare
1 Pingback