Teil 4: Mehr Orte, mehr Möglichkeiten – Adresssuche hinzufügen
Hallo und willkommen zurück zu unserer Flutter-Wetter-App-Serie!
Im letzten Teil haben wir einen riesigen Schritt gemacht: Wir haben die Standard-Demo-App durch unsere eigene ersetzt, eine solide Architektur aufgebaut und die aktuelle Temperatur für unseren GPS-Standort angezeigt. Dabei haben wir viel über eine saubere Architektur, State Management mit Riverpod und Fehlerbehandlung gelernt.
Doch eine Wetter-App ist erst richtig nützlich, wenn wir das Wetter für beliebige Orte nachschlagen können. Genau das packen wir heute an! Wir erweitern unsere App um eine Adresssuche.

Das Ziel für heute:
- Wir untersuchen den Code für Teil 4 aus unserem Repository.
- Wir verstehen, wie das Suchfeld zur Benutzeroberfläche hinzugefügt wurde.
- Wir lernen Geocoding: Wie eine Adresse („München“) in Koordinaten (Latitude/Longitude) umgewandelt wird.
- Wir verfolgen, wie die App-Logik und Datenbeschaffung angepasst wurden, um Wetterdaten für gesuchte Orte abzurufen.
- Wir sehen, wie Fehler bei der Suche (z.B. „Adresse nicht gefunden“) behandelt werden.
Warum dieser Schritt wichtig ist:
Dieser Teil zeigt, wie eine gut geplante Architektur Früchte trägt. Wir müssen nicht alles neu erfinden, sondern können auf unserem bestehenden Fundament aufbauen und gezielt Erweiterungen hinzufügen. Außerdem lernen wir eine weitere wichtige Funktion vieler Apps kennen: die Verarbeitung von Benutzereingaben und die Umwandlung von Text in geografische Daten.
Schritt 1: Den Code für Teil 4 holen
Der gesamte Code für diesen Beitrag ist wieder im Git-Repository vorbereitet.
1. Öffne ein Terminal in deinem Projektordner (flutter_weather_app_blog
).
2. Stelle sicher, dass du keine lokalen Änderungen hast, die du behalten willst (ggf. git stash
).
3. Wechsle zum main
-Branch (um sicher vom letzten stabilen Punkt auszugehen):
git checkout main git pull origin main
4. Checke den Code-Stand für Teil 4 aus:
git checkout part4-address-search
(Zur Erinnerung: Die ‚detached HEAD‘-Meldung ist normal und bedeutet, dass du einen spezifischen Punkt in der Projekthistorie betrachtest.)
5. Ganz wichtig: Abhängigkeiten holen & Code generieren: Wir haben eine neue Bibliothek hinzugefügt und Code geändert, der generiert werden muss. Führe diese beiden Befehle aus:
flutter pub get dart run build_runner build --delete-conflicting-outputs
Öffne das Projekt jetzt in VS Code. Lass uns die Neuerungen erkunden!
Schritt 2: Was ist neu? Der Überblick
Wenn du die App jetzt startest (F5
in VS Code), wirst du sofort die Änderungen sehen:
- Ein Suchfeld ist oben erschienen.
- Ein „Mein Standort“-Button (ein kleines Fadenkreuz-Icon) befindet sich nun oben rechts in der AppBar.
Unter der Oberfläche sind die wesentlichen Anpassungen:
- Neues Paket: Wir nutzen das
geocoding
-Paket. - UI-Anpassungen: Der Hauptbildschirm (
WeatherScreen
) wurde umgebaut, um das Suchfeld und den neuen Button zu integrieren. Ein neues Widget (SearchBarWidget
) wurde erstellt. - Geocoding-Logik: Neue Funktionen im
LocationService
undWeatherRepository
wandeln Adressen in Koordinaten um. - State Management Update: Der
WeatherNotifier
kann nun Suchanfragen verarbeiten. - Intelligentere Refresh-Funktion: Aktualisiert jetzt immer den zuletzt angezeigten Ort.
- Erweiterte Fehlerbehandlung: Erkennt und meldet Fehler bei der Adresssuche.
Schritt 3: Die neue Zutat – Das geocoding
-Paket
Wie wird aus dem Text „Hamburg“ ein Paar von Koordinaten (Breitengrad, Längengrad), das unsere Wetter-API versteht? Durch Geocoding.
pubspec.yaml
: In dieser Datei (dem „Rezeptbuch“ unserer App) haben wir eine neue Zutat hinzugefügt:
dependencies: # … andere Pakete … geocoding: ^3.0.0 # Version prüfen
Dieses Paket erlaubt uns, die eingebauten Geocoding-Funktionen von Android zu verwenden. Der Befehl flutter pub get
hat dieses Paket heruntergeladen und für unser Projekt verfügbar gemacht.
Schritt 4: Die Benutzeroberfläche (presentation
) – Ein Platz für die Suche
Schauen wir uns an, wie die Benutzeroberfläche in lib/src/features/weather/presentation/
angepasst wurde.
widgets/search_bar.dart
(NEU):
Um den Code aufgeräumt zu halten, haben wir ein eigenes Widget für die Suchleiste erstellt. Es ist ein einfaches TextField
, aber schön verpackt mit einem Such-Icon (prefixIcon
), einem Senden-Icon (suffixIcon
) und abgerundeten Ecken (InputDecoration
).
Wichtige Parameter:
controller
: EinTextEditingController
, um den Text im Feld zu steuern.onSearch
: Eine Funktion, die aufgerufen wird, wenn der Nutzer auf das Senden-Icon tippt oder auf der Tastatur „Suchen“ auswählt.isLoading
: Einbool
, um das Feld und den Button zu deaktivieren, während die App Daten lädt.
// lib/src/features/weather/presentation/widgets/search_bar.dart import 'package:flutter/material.dart';</li> </ul> </li> </ul> class SearchBarWidget extends StatelessWidget { final TextEditingController controller; final Function(String) onSearch; final bool isLoading; final String hintText; const SearchBarWidget({ /* … Konstruktor … */ }); @override Widget build(BuildContext context) { return TextField( controller: controller, enabled: !isLoading, // Deaktivieren bei Ladevorgang decoration: InputDecoration( hintText: hintText, prefixIcon: const Icon(Icons.search), // … weitere Styling-Details … suffixIcon: isLoading ? /* Ladeindikator */ : IconButton( icon: const Icon(Icons.send), onPressed: isLoading ? null : () => onSearch(controller.text), ), ), textInputAction: TextInputAction.search, onSubmitted: isLoading ? null : onSearch, // Suche auch bei Enter ); } }
screens/weather_screen.dart
: Hier passieren mehrere Dinge:
- Controller Management: Im
_WeatherScreenState
wird einTextEditingController
erstellt:
final TextEditingController _searchController = TextEditingController();
- Ganz wichtig: Controller speichern Ressourcen und müssen freigegeben werden, wenn das Widget zerstört wird. Das geschieht in der
dispose
-Methode:
@override void dispose() { _searchController.dispose(); // Controller aufräumen! super.dispose(); }
- Layout Anpassung: Die
build
-Methode des Screens enthält jetzt eineColumn
. Das erste Kind dieserColumn
ist einPadding
, das unser neuesSearchBarWidget
enthält. Darunter folgen derLocationHeader
und derExpanded
-Bereich für den Hauptinhalt (Wetteranzeige oder Fehler).
// lib/src/features/weather/presentation/screens/weather_screen.dart (Ausschnitt build) @override Widget build(BuildContext context) { final weatherState = ref.watch(weatherNotifierProvider); final weatherNotifier = ref.read(weatherNotifierProvider.notifier); return Scaffold( appBar: AppBar( /* … mit Buttons … <em>/ ), body: Column( // NEU: Column für vertikale Anordnung children: [ // Suchleiste oben Padding( padding: const EdgeInsets.all(12.0), child: SearchBarWidget( controller: _searchController, isLoading: weatherState.status == WeatherStatus.loading, onSearch: (query) { // Was passiert bei Suche? if (query.trim().isNotEmpty) { _log.info('Screen: Suche ausgelöst für "$query"'); FocusScope.of(context).unfocus(); // Tastatur weg // Aktion im Notifier auslösen! weatherNotifier.fetchWeatherForAddress(query); } else { _showSnackbar("Bitte einen Ort oder eine Adresse eingeben."); } }, ), ), // Ort anzeigen Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), child: LocationHeader( /</em> … <em>/ ), ), // Restlicher Inhalt (nimmt verfügbaren Platz ein) Expanded( child: RefreshIndicator( /</em> … */ ), ), ], ), ); }
- Neue AppBar-Aktion: Der „Mein Standort“-Button (
IconButton
mitIcons.my_location
) wurde zurAppBar
hinzugefügt. SeinonPressed
ruft einfachweatherNotifier.fetchWeatherForCurrentLocation()
, die Methode, die wir schon aus Teil 3 kennen.
- Fehlerbehandlung: Das
_buildErrorWidget
wurde angepasst. Es prüft nun zusätzlich, ob derfailure
vom TypGeocodingFailure
ist. Wenn ja, zeigt es eine spezifische Meldung (z.B. „Adresse ‚XYZ‘ konnte nicht gefunden werden.“) und ein passendes Icon (Icons.wrong_location_outlined
). Da der Nutzer in diesem Fall eine neue, korrekte Suche starten muss, gibt es hier keinen „Erneut versuchen“-Button.
Schritt 5: Die Datenbeschaffung (core
, data
, domain
) – Adresse zu Koordinaten
Das ist das technische Herzstück der neuen Funktion. Wie kommen wir von „Paris“ zu Längen- und Breitengrad?
LocationService
(lib/src/core/location/location_service.dart
):- Dieser Service ist nun der Experte für alle Standort-bezogenen Aufgaben. Er hat die neue Methode
getCoordinatesFromAddress
bekommen. - Wie sie funktioniert: Sie nutzt intern die Funktion
locationFromAddress
aus demgeocoding
-Paket. Dieses Paket kommuniziert mit den nativen Diensten von Android, um die Koordinaten zu ermitteln. - Fehlerbehandlung: Wenn das
geocoding
-Paket die Adresse nicht kennt, wirft es eineNoResultFoundException
. UnserLocationService
fängt diesen spezifischen Fehler und wirft stattdessen unsere eigene, definierteGeocodingException
. Das ist wichtig, damit der Rest unserer App nicht die Interna desgeocoding
-Pakets kennen muss. Andere Fehler (z.B. Netzwerkprobleme während des Geocodings) werden ebenfalls alsGeocodingException
gefangen. - Reverse Geocoding: Die Methode
getAddressFromCoordinates
, die wir in Teil 3 nur als Platzhalter hatten, ist jetzt auch implementiert. Sie nutztplacemarkFromCoordinates
aus demgeocoding
-Paket, um aus Koordinaten wieder eine lesbare Adresse zu machen. Das wird genutzt, um den Anzeigenamen imLocationHeader
potenziell zu verbessern (z.B. aus Koordinaten wird „Berlin, Deutschland“).
- Dieser Service ist nun der Experte für alle Standort-bezogenen Aufgaben. Er hat die neue Methode
- Fehlerklassen (
lib/src/core/error/
):- Wir haben
GeocodingException
(für technische Geocoding-Fehler) undGeocodingFailure
(die abstraktere Form für unsere App-Logik) hinzugefügt.
- Wir haben
WeatherRepository
(Interface & Implementierung):- Das Interface (
domain/repositories/weather_repository.dart
) wurde um die MethodegetCoordinatesForAddress
erweitert – unser Repository muss diese Fähigkeit nun anbieten. - Die Implementierung (
data/repositories/weather_repository_impl.dart
) erfüllt diesen Vertrag:- Die
getCoordinatesForAddress
-Methode ruft_locationService.getCoordinatesFromAddress
auf. - Sie fängt die
GeocodingException
vom Service. - Sie gibt das Ergebnis als
Either
zurück:Left(GeocodingFailure(...))
im Fehlerfall oderRight(LocationInfo(...))
bei Erfolg. - Wichtig: Bei Erfolg versucht sie noch, den
displayName
desLocationInfo
-Objekts durch einen Aufruf vongetLocationDisplayName
(also Reverse Geocoding) zu verfeinern, bevor sie das Ergebnis zurückgibt.
- Die
- Das Interface (
Schritt 6: Die Logik (application
) – Suche und Wetter verknüpfen
Der WeatherNotifier
(lib/src/features/weather/application/weather_notifier.dart
) dirigiert das Zusammenspiel.
- Neue Methode
fetchWeatherForAddress
:- Das ist die Reaktion auf die Nutzereingabe im Suchfeld.
- Sie prüft, ob überhaupt etwas eingegeben wurde und ob gerade schon geladen wird.
- Sie setzt den
state
aufloading
. - Kernschritt 1: Sie ruft
_weatherRepository.getCoordinatesForAddress(address)
auf. - Kernschritt 2: Sie wertet das zurückgegebene
Either
aus:- Bei
Left(failure)
(z.B.GeocodingFailure
): Derstate
wird auffailure
gesetzt, die UI zeigt den Fehler. - Bei
Right(locationInfo)
(Koordinaten erfolgreich gefunden): Jetzt kommt der Clou! Sie ruft die bereits vorhandene Methode_fetchWeatherDataAndUpdateState(locationInfo)
auf. Diese Methode weiß, wie man Wetterdaten für einLocationInfo
-Objekt holt – egal, ob dieses vom GPS oder vom Geocoding kam. Das ist Wiederverwendung dank guter Architektur!
- Bei
- Angepasste Methode
refreshWeatherData
:- Diese Methode wurde intelligenter. Zuvor hat sie einfach immer den GPS-Standort neu geladen.
- Jetzt: Sie schaut sich den
state.selectedLocation
an (der ja nach einer erfolgreichen Suche den gesuchten Ort enthält). Sie ruft dann_fetchWeatherDataAndUpdateState
für genau diesen gespeicherten Ort auf. Damit funktioniert der Refresh-Button und Pull-to-Refresh immer für den Ort, den der Nutzer gerade sieht.
Schritt 7: Der Datenfluss bei der Suche (Zusammenfassung)
Lass uns den Weg einer Suche nachvollziehen:
- Eingabe: Nutzer tippt „Rom“ ein und drückt Senden (
WeatherScreen
). - Aktion:
onSearch
ruftnotifier.fetchWeatherForAddress("Rom")
. - Laden: Notifier setzt
state
aufloading
(UI zeigt Ladekreis). - Koordinaten holen: Notifier ruft
repo.getCoordinatesForAddress("Rom")
. - Geocoding: Repo ruft
service.getCoordinatesFromAddress("Rom")
. Service ruftgeocoding
-Paket, bekommt Koordinaten für Rom. - Reverse Geocoding: Repo ruft
service.getAddressFromCoordinates(...)
, bekommt vielleicht „Rom, Latium, Italien“ zurück. - Erfolg (Koordinaten): Repo gibt
Right(LocationInfo(..., displayName: "Rom, Latium, Italien"))
an Notifier zurück. - Wetter holen: Notifier erhält
locationInfo_Rom
und ruft_fetchWeatherDataAndUpdateState(locationInfo_Rom)
. - API-Anfrage: Notifier ruft
repo.getWeatherForLocation(locationInfo_Rom)
. Repo ruftapiService.getCurrentWeather(...)
mit Rom-Koordinaten. - API-Antwort: ApiService holt Daten von Open-Meteo, parst sie.
- Erfolg (Wetter): Repo gibt
Right(weatherData_Rom)
an Notifier zurück. - Finaler State: Notifier setzt
state
aufsuccess
mitweatherData_Rom
undlocationInfo_Rom
. - Anzeige:
WeatherScreen
wird neu gebaut und zeigt „Rom, Latium, Italien“ und die aktuelle Temperatur für Rom an.
Schritt 8: Ausführen und Experimentieren!
Starte die App (F5
) und spiele damit herum:
- Suche verschiedene Städte, Länder, Sehenswürdigkeiten.
- Suche nach ungültigen Adressen.
- Wechsle zwischen Suchergebnissen und deinem GPS-Standort über den „Mein Standort“-Button.
- Teste die Refresh-Funktion für gesuchte Orte.
- Beobachte die Log-Ausgaben in der „DEBUG CONSOLE“ in VS Code, um den Fluss nachzuvollziehen.
Fazit und Ausblick
Wir haben unserer App eine wichtige Funktion hinzugefügt und dabei gesehen, wie unsere Architektur uns geholfen hat, Code wiederzuverwenden und die Änderungen sauber zu integrieren. Du hast gelernt:
- Wie man eine neue Bibliothek (
geocoding
) einbindet und nutzt. - Wie der Prozess von der Adresseingabe bis zur Koordinate funktioniert (Geocoding).
- Wie man die App-Logik im Notifier erweitert, um neue Benutzeraktionen zu behandeln.
- Wie man bestehende Komponenten (wie die Wetterabfrage) für neue Szenarien wiederverwendet.
- Wie man die Refresh-Logik an den aktuellen App-Zustand anpasst.
Unsere App ist jetzt deutlich flexibler. Aber reine Zahlen und Texte sind oft nicht genug, um Wettertrends zu erfassen. Im nächsten Teil (Teil 5) wird es grafisch: Wir holen uns stündliche Temperaturdaten von der API und visualisieren den Verlauf der letzten 7 Tage und die Prognose für die nächsten 7 Tage in einem interaktiven Liniendiagramm!
Das wird die App optisch aufwerten und noch informativer machen. Bleib neugierig!
Weiterführende Ressourcen & Vertiefung (für Teil 4)
In diesem Teil haben wir die App interaktiver gemacht und Geocoding eingeführt.
- Geocoding:
geocoding
Paket: https://pub.dev/packages/geocoding – Die Dokumentation zum verwendeten Paket.- Konzept Geocoding/Reverse Geocoding (Allgemein): Suche online nach diesen Begriffen, um das grundlegende Konzept besser zu verstehen (z.B. auf Wikipedia oder GIS-Seiten).
- Flutter UI – Eingabefelder:
TextField
Widget: https://api.flutter.dev/flutter/material/TextField-class.html – Die offizielle API-Dokumentation für das Text-Eingabefeld.TextEditingController
: https://api.flutter.dev/flutter/widgets/TextEditingController-class.html – Wichtig zur Steuerung des Textes in einemTextField
. Denke andispose()
!- Formulareingaben verarbeiten (Flutter Kochbuch): https://docs.flutter.dev/cookbook/forms/retrieve-input – Zeigt verschiedene Wege, auf Benutzereingaben zu reagieren.
- Fokus Management (
FocusScope
): https://api.flutter.dev/flutter/widgets/FocusScope-class.html – Nützlich, um z.B. die Tastatur nach der Eingabe zu schließen.
- Architektur & Refactoring:
- (Die Links aus Teil 3 zur Architektur sind weiterhin relevant) – Betrachte, wie die bestehende Struktur das Hinzufügen der Suchfunktion erleichtert hat, ohne große Umbauten an der Wetterabfrage selbst vornehmen zu müssen.
Schreibe einen Kommentar