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.dart
enden).
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 einerProviderScope
(notwendig für Riverpod).lib/app.dart
: Enthält dasMaterialApp
-Widget, das die grundlegende Struktur, das Theme (Aussehen) und die Startseite (WeatherScreen
) unserer App definiert.lib/src/core/
: Unser Fundament.error/
: Enthältexceptions.dart
(spezifische technische Fehler wieNetworkException
) undfailure.dart
(abstraktere Fehler wieNetworkFailure
, die die Logik verstehen kann). Diese Trennung hilft, Fehler sauber zu behandeln.location/
:location_service.dart
kapselt die Interaktion mit demgeolocator
-Paket. Alle GPS- und Berechtigungs-Anfragen laufen hierüber. Wird über Riverpod bereitgestellt (locationServiceProvider
).networking/
:http_client.dart
stellt eine globale Instanz deshttp.Client
bereit (überhttpClientProvider
). Das macht es einfach, ihn in Tests durch einen Mock zu ersetzen.utils/
: Enthält Helfer wielogger.dart
(für strukturierte Logs) unddate_formatter.dart
(um z.B. die Uhrzeit anzuzeigen).constants/
:app_constants.dart
sammelt zentrale Konstanten (hier erstmal nurmyLocationLabel
).
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 enthaltenfromJson
-Methoden, um JSON in Dart-Objekte umzuwandeln.datasources/
:weather_api_service.dart
. Spricht über denhttp.Client
mit der Open-Meteo API (getCurrentWeather
-Methode). Parst die JSON-Antwort mithilfe derModels
und wirft spezifischeExceptions
(ApiException
,NetworkException
,DataParsingException
). Wird über Riverpod bereitgestellt (weatherApiServiceProvider
).repositories/
:weather_repository_impl.dart
. Die konkrete Implementierung unseres Datenvertrags. Diese Klasse kennt denWeatherApiService
und denLocationService
. Sie ruft deren Methoden auf, fängt derenExceptions
ab und wandelt sie inFailures
um (z.B. wirdNetworkException
zuNetworkFailure
). Sie wandelt auch die API-Models
in die App-internenEntities
um. 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. NutzenEquatable
fü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 dieWeatherRepositoryImpl
erfüllen muss.
application/
: Die Orchestrierung.weather_state.dart
: Definiert, wie der Zustand des Wetter-Features aussieht: einenum WeatherStatus
(initial, loading, success, failure), die eigentlichenCurrentWeatherData
, derselectedLocation
und ein optionalesFailure
-Objekt. NutztEquatable
undcopyWith
.weather_notifier.dart
: Die Steuerzentrale. EinStateNotifier
, der denWeatherState
hält und aktualisiert. Er bekommt dasWeatherRepository
übergeben (Dependency Injection durch Riverpod). Enthält die Logik fürfetchWeatherForCurrentLocation
undrefreshWeatherData
. Er ruft Methoden im Repository auf, behandelt dasEither<Failure, Success>
-Ergebnis und aktualisiert denstate
entsprechend.
presentation/
: Die Benutzeroberfläche.providers/
:weather_providers.dart
. Definiert denweatherNotifierProvider
, der denWeatherNotifier
erstellt und der UI zur Verfügung stellt.widgets/
: Kleine, wiederverwendbare UI-Teile.location_header.dart
zeigt den Ortsnamen,current_temperature_display.dart
zeigt die Temperatur und Zeit. Sie bekommen ihre Daten von außen übergeben.screens/
:weather_screen.dart
. Der Hauptbildschirm. EinConsumerStatefulWidget
, das überref.watch(weatherNotifierProvider)
den aktuellenWeatherState
bekommt. Basierend auf demstatus
im State, zeigt es entweder einen Ladeindikator, die Wetter-Widgets oder eine Fehlermeldung (_buildErrorWidget
). Es nutztref.read(weatherNotifierProvider.notifier)
um Aktionen im Notifier auszulösen (initiales Laden, Refresh). DerRefreshIndicator
ermöglicht Pull-to-Refresh. DerAnimatedSwitcher
sorgt 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,ProviderScope
wird erstellt.WeatherScreen
wird 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 seinenstate
aufstatus: WeatherStatus.loading
. - UI Reaction (
WeatherScreen
): Da der Screen viaref.watch
auf 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
LocationService
interagiert mit demgeolocator
, fragt ggf. nach Berechtigungen und holt diePosition
. - Wenn erfolgreich, gibt der Service die
Position
zurü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 esLocationInfo
und gibtRight(locationInfo)
zurück.
- Notifier ruft
- Handle Coordinates Result (
WeatherNotifier
):- Der Notifier erhält das
Either
. BeiLeft(failure)
setzt er denstate
aufstatus: WeatherStatus.failure
mit demfailure
-Objekt -> Die UI zeigt die Fehlermeldung. - Bei
Right(locationInfo)
geht es weiter. Der Notifier versucht noch, den Namen via_weatherRepository.getLocationDisplayName
zu 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
WeatherApiService
baut 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 esCurrentWeatherModel
inCurrentWeatherData
(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 denstate
aufstatus: WeatherStatus.failure
mit demfailure
-Objekt -> Die UI zeigt die Fehlermeldung. - Bei
Right(weatherData)
setzt er denstate
aufstatus: WeatherStatus.success
, speichertweatherData
undlocationInfo
im 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,_buildContent
erkenntWeatherStatus.success
und zeigt jetztLocationHeader
undCurrentTemperatureDisplay
mit den Daten aus demstate
an.
Schritt 5: Schlüsselkonzepte im Code
- Riverpod für State Management & Dependency Injection:
ProviderScope
(inmain.dart
): Macht Provider global verfügbar.@riverpod
/...Provider
(z.B. inlocation_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 denhttp.Client
oder denLocationService
) „injiziert“, statt sie selbst zu erstellen.StateNotifierProvider
(weather_providers.dart
): Ein spezieller Provider für unserenWeatherNotifier
, der dessen Zustand (WeatherState
) verwaltet.ConsumerWidget
/ConsumerStatefulWidget
(WeatherScreen
): Widgets, die auf Provider „hören“ können.ref.watch()
(imbuild
vonWeatherScreen
): Liest den Wert eines Providers und baut das Widget neu, wenn sich der Wert (hier derWeatherState
) ändert.ref.read()
(iminitState
oder in Callbacks wieonPressed
vonWeatherScreen
): 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 spezifischeExceptions
bei technischen Problemen. - Das Repository (
WeatherRepositoryImpl
) fängt dieseExceptions
und wandelt sie in allgemeinereFailures
um. 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 denWeatherState
entsprechend (status: WeatherStatus.failure
odersuccess
). - Die UI (
WeatherScreen
) reagiert auf denstatus
und daserror
-Feld im State und zeigt die passende Ansicht.
- Services (
- Asynchronität (
Future
,async
,await
): Netzwerk- und GPS-Anfragen dauern eine Weile.Future
repräsentiert einen Wert, der irgendwann verfügbar sein wird.async
markiert eine Funktion, dieawait
verwenden kann.await
pausiert die Ausführung der Funktion, bis derFuture
abgeschlossen ist, ohne die gesamte App zu blockieren. Wir sehen das intensiv im Notifier, Repository und den Services. - Immutability & Equatable: Der
WeatherState
wird nie direkt geändert. Stattdessen wird mitcopyWith
eine neue Instanz mit den geänderten Werten erstellt. Das macht den Zustandsfluss vorhersagbar.Equatable
hilft 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
AppLogger
aus 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
WeatherApiService
oder 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!
Schreibe einen Kommentar