Navigation und API
Die App bekommt eine Städteliste, die wir mit dem Navigator und der
Wetteranzeige verbinden. Danach ersetzen wir die Fake-Daten durch echte
Wetterdaten. Die API-Anbindung übernimmt ein Coding Agent; unsere
Aufgabe ist, seinen Plan und seinen Code zu prüfen.
Listen mit ListView
ListView.builder erzeugt Einträge erst, wenn sie sichtbar werden. Damit bleiben auch lange Listen performant.
ListView.separated(
itemCount: cities.length,
itemBuilder: (context, index) {
final city = cities[index];
return ListTile(
title: Text(city.name),
onTap: () { /* Navigation, siehe unten */ },
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
)Neue Datei für die Städteliste. Die Navigation in
onTap erklärt der nächste Abschnitt; WeatherScreen(city: city)
meldet einen Fehler, bis WeatherScreen das Feld city hat (siehe
Laden beim Öffnen).
Code: lib/ui/city_list_screen.dart (schritt-08)
lib/ui/city_list_screen.dart (schritt-08)import 'package:flutter/material.dart';
import '../model/city.dart';
import 'weather_screen.dart';
class CityListScreen extends StatelessWidget {
const CityListScreen({super.key, required this.cities});
final List<City> cities;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Städte')),
body: ListView.separated(
itemCount: cities.length,
itemBuilder: (context, index) {
final city = cities[index];
return ListTile(
title: Text(city.name),
subtitle: Text(
'${city.latitude.toStringAsFixed(2)}° N, '
'${city.longitude.toStringAsFixed(2)}° O',
),
// Navigieren = neue Route auf den Stack legen
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WeatherScreen(city: city),
),
),
);
},
separatorBuilder: (context, index) => const Divider(height: 1),
),
);
}
}main.dart startet jetzt mit der Städteliste:
Code: lib/main.dart (schritt-08)
lib/main.dart (schritt-08)import 'package:flutter/material.dart';
import 'data/fake_data.dart';
import 'ui/city_list_screen.dart';
void main() {
runApp(const WeatherApp());
}
class WeatherApp extends StatelessWidget {
const WeatherApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Wetter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const CityListScreen(cities: fakeCities),
);
}
}ListView.separated ist eine Variante von ListView.builder, die
zwischen zwei Einträgen einen Trenner einfügt, hier einen Divider.
Ein ListView(children: [...]) würde wie eine Column alle Einträge
auf einmal erzeugen. Für wenige, feste Elemente reicht das, für lange
Listen nicht.
ListTile ist die fertige Material-Zeile mit Titel, Untertitel und
onTap, wie ListItem in Compose. Der Untertitel zeigt die
Koordinaten. Zwei String-Literale direkt hintereinander verbindet Dart automatisch.
Einen key wie in der LazyColumn braucht die Liste nicht, solange sie
sich nicht ändert. Bei veränderlichen Listen hilft ein ValueKey
Flutter, Einträge wiederzuerkennen.
Navigation mit Navigator
Der Navigator verwaltet einen Stapel von Screens (Routes).
- Navigieren: Route auf den Stapel legen,
Navigator.push(…) - Zurück: oberste Route entfernen,
Navigator.pop(context) - Angezeigt wird immer die oberste Route
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => WeatherScreen(city: city)),
),MaterialApp bringt den Navigator schon mit, eine zusätzliche
Abhängigkeit braucht es nicht. Navigator.push findet ihn über den
context, die Position des Widgets im Baum. MaterialPageRoute erzeugt
den Screen und animiert den Übergang passend zur Plattform.
Der Zurück-Pfeil in der AppBar erscheint automatisch, sobald eine
Route unter der aktuellen liegt. Zurück-Pfeil und Zurück-Geste des
Systems rufen beide Navigator.pop auf.
Anders als bei Navigation 3 ist der Stapel hier kein State der App,
sondern steckt im Navigator. Navigiert wird imperativ mit
Befehlen wie push und pop. Für Deep Links oder Web-URLs gibt es
zusätzlich eine deklarative Variante; am bekanntesten ist das Paket
go_router , das Routen als Pfade wie
/city/loerrach beschreibt. Für unsere zwei Screens reicht push.
| Navigation 3 (Compose) | Navigator (Flutter) |
|---|---|
| Back Stack als Liste im State | Stapel im Navigator |
backStack.add(CityWeather(city)) | Navigator.push(context, MaterialPageRoute(…)) |
backStack.removeLastOrNull() | Navigator.pop(context) |
Routen als @Serializable-Typen | Route mit builder, Argumente über den Konstruktor |
| Zurück-Pfeil selbst einbauen | Zurück-Pfeil automatisch |
Hier navigiert CityListScreen selbst. In der Android-App entscheidet
der Aufrufer über onCityClick; das ginge auch hier, wäre aber für eine
App mit zwei Screens mehr Code, als es nützt.
Laden beim Öffnen
Jeder Wetter-Screen erzeugt sein eigenes ViewModel und lädt das Wetter in initState.
class _WeatherScreenState extends State<WeatherScreen> {
final _viewModel = WeatherViewModel();
@override
void initState() {
super.initState();
_viewModel.loadWeather(widget.city);
}
// dispose() und build() wie bisher
}WeatherScreen und WeatherContent bekommen dafür ein neues Feld
City city. Statt „Wetter“ zeigt die AppBar jetzt city.name; so
steht der Name der Stadt schon während des Ladens da. Die Stadt für die
Karte kommt deshalb auch nicht mehr aus WeatherSuccess, das Pattern
holt nur noch weather heraus.
Der obere Teil der Datei (geänderte Zeilen
hervorgehoben), UnitToggle und CurrentWeatherCard bleiben
unverändert:
Code: oberer Teil von lib/ui/weather_screen.dart (schritt-08)
lib/ui/weather_screen.dart (schritt-08)import 'package:flutter/material.dart';
import '../model/city.dart';
import '../model/current_weather.dart';
import 'weather_condition_ui.dart';
import 'weather_ui_state.dart';
import 'weather_view_model.dart';
// Stateful: besitzt das ViewModel und den UI-State für °C/°F
class WeatherScreen extends StatefulWidget {
const WeatherScreen({super.key, required this.city});
final City city;
@override
State<WeatherScreen> createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
final _viewModel = WeatherViewModel();
// UI-Element-State: bleibt im Widget, gehört nicht ins ViewModel
bool _useCelsius = true;
@override
void initState() {
super.initState();
_viewModel.loadWeather(widget.city);
}
@override
void dispose() {
_viewModel.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Baut neu, sobald das ViewModel notifyListeners() aufruft
return ListenableBuilder(
listenable: _viewModel,
builder: (context, _) => WeatherContent(
city: widget.city,
uiState: _viewModel.uiState,
useCelsius: _useCelsius,
onToggleUnit: () => setState(() => _useCelsius = !_useCelsius),
),
);
}
}
// Stateless: zeigt nur an, was es bekommt
class WeatherContent extends StatelessWidget {
const WeatherContent({
super.key,
required this.city,
required this.uiState,
required this.useCelsius,
required this.onToggleUnit,
});
final City city;
final WeatherUiState uiState;
final bool useCelsius;
final VoidCallback onToggleUnit;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(city.name),
actions: [UnitToggle(useCelsius: useCelsius, onToggle: onToggleUnit)],
),
body: switch (uiState) {
WeatherLoading() => const Center(child: CircularProgressIndicator()),
// Scrollbar, damit die Karte im Querformat nicht überläuft
WeatherSuccess(:final weather) => SingleChildScrollView(
child: CurrentWeatherCard(
city: city,
weather: weather,
useCelsius: useCelsius,
),
),
WeatherError(:final message) => Center(child: Text(message)),
},
);
}
}
// UnitToggle und CurrentWeatherCard unverändertVollständige Datei:
ui/weather_screen.dart in schritt-08
initState wird einmal aufgerufen, wenn der State entsteht, also beim
Öffnen des Screens. Der Konstruktor des WeatherViewModel, der bisher
Lörrach geladen hat, lädt deshalb nichts mehr.
Das ist sauberer als in der Android-App, wo ein gemeinsames ViewModel beim Antippen geladen wird: Das ViewModel lebt genau so lange wie der Screen. Wechselt man schnell zwischen Städten, kann keine alte Antwort im neuen Screen landen.
Eine Stolperfalle bleibt: Verlässt man den Screen, während noch geladen
wird, gibt dispose() das ViewModel frei. Kommt die Antwort danach an,
ruft loadWeather notifyListeners() auf einem freigegebenen
ChangeNotifier auf, und Flutter meldet im Debug-Modus einen Fehler
(„A WeatherViewModel was used after being disposed“). Ein Future lässt
sich nicht abbrechen. Deshalb merkt sich das ViewModel, ob es
freigegeben wurde:
bool _disposed = false;
@override
void dispose() {
_disposed = true;
super.dispose();
}
// am Ende von loadWeather:
if (!_disposed) notifyListeners();Nachstellen: die Abfrage if (!_disposed) entfernen, eine Stadt öffnen
und sofort zurückgehen.
Das ViewModel nach diesem Schritt. Der Import von fake_data.dart
entfällt mit dem Laden im Konstruktor:
Code: lib/ui/weather_view_model.dart (schritt-08)
lib/ui/weather_view_model.dart (schritt-08)import 'package:flutter/foundation.dart';
import '../data/fake_weather_repository.dart';
import '../data/weather_repository.dart';
import '../model/city.dart';
import 'weather_ui_state.dart';
class WeatherViewModel extends ChangeNotifier {
WeatherViewModel({WeatherRepository? repository})
: _repository = repository ?? FakeWeatherRepository();
final WeatherRepository _repository;
WeatherUiState _uiState = const WeatherLoading();
WeatherUiState get uiState => _uiState;
// Wird der Screen geschlossen, während noch geladen wird, darf danach
// notifyListeners() nicht mehr aufgerufen werden
bool _disposed = false;
@override
void dispose() {
_disposed = true;
super.dispose();
}
Future<void> loadWeather(City city) async {
_uiState = const WeatherLoading();
notifyListeners();
try {
final weather = await _repository.getCurrentWeather(city);
_uiState = WeatherSuccess(city, weather);
} catch (e) {
_uiState = WeatherError('$e');
}
if (!_disposed) notifyListeners();
}
}Code: schritt-08 ·
Änderungen
API-Anbindung mit einem Coding Agent
Aufgabe für den Agent: ein ApiWeatherRepository für Open-Meteo bauen und im ViewModel einsetzen.
- Plan anfordern und prüfen, bevor Code entsteht
- umsetzen lassen,
flutter analyze, App testen - Diff lesen und erklären können
- erst dann committen
Die Datei AGENTS.md im Repository gibt dem Agent Regeln vor, z. B.
das Paket http statt Dio, kein Provider/Riverpod, kein Code-Generator,
die UI nicht anfassen, nicht selbst committen. Gängige Agents lesen sie
automatisch, z. B. Claude Code oder GitHub Copilot in VS Code.
Hinein gehört nur, was der Agent nicht aus dem Code ablesen kann: Abweichungen vom Üblichen und Regeln für die Zusammenarbeit. Eine Beschreibung des Projekts oder der Architektur hilft dagegen kaum, denn der Agent liest den Code ohnehin. Jede Zeile kostet bei jeder Anfrage Tokens.
Die Datei liegt im Projektordner, neben pubspec.yaml:
# AGENTS.md
Lern-Projekt: Die Personen hier lernen gerade Flutter-Entwicklung.
Erkläre deine Änderungen so, dass sie nachvollziehbar sind.
## Regeln
- HTTP mit dem Paket `http` und `dart:convert`.
Kein Dio, kein Provider/Riverpod/Bloc, kein Code-Generator (json_serializable, freezed).
- Keine weiteren Pakete, ohne zu begründen, warum, und nachzufragen.
- API-Antworten als eigene DTO-Klassen abbilden und auf `model/` mappen.
Das Datenmodell nicht an die API anpassen.
- Änderungen an der Datenschicht (`data/`) dürfen die UI (`ui/`) nicht
verändern. Wenn das doch nötig scheint, vorher nachfragen.
- State-Management mit `ChangeNotifier` und `ListenableBuilder` (ViewModel pro Screen).
- Texte in der App auf Deutsch.
## Arbeitsweise
- Vor größeren Änderungen einen kurzen Plan vorlegen und auf Bestätigung warten.
- Nach jeder Änderung prüfen: `dart format lib` und `flutter analyze`
- Nicht selbst committen; die Person prüft den Diff und committet.Ein möglicher Prompt:
Baue ein
ApiWeatherRepository, das das aktuelle Wetter einer Stadt von Open-Meteo lädt (Temperatur, Wettercode, Windgeschwindigkeit; Koordinaten ausCity). Setze es als Default imWeatherViewModelein. Zeig mir zuerst deinen Plan.
Die API braucht keinen Schlüssel. Eine Antwort lässt sich vorab im Browser ansehen: api.open-meteo.com/v1/forecast?latitude=47.62&longitude=7.66¤t=temperature_2m,weather_code,wind_speed_10m
Das Paket http
Pakete kommen von pub.dev und werden mit flutter pub add eingebunden.
Den Code im Referenz-Repository hat ein Coding Agent geschrieben. Wer ohne Agent nachbaut, kann die folgenden Dateien direkt übernehmen.
Neue Datei, erster Teil. Die
Extension toCurrentWeather und die Funktion
_weatherConditionFromWmoCode am Ende der Datei folgen im Abschnitt
DTO und Datenmodell:
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../model/city.dart';
import '../model/current_weather.dart';
import 'api/open_meteo_dto.dart';
import 'weather_repository.dart';
class ApiWeatherRepository implements WeatherRepository {
ApiWeatherRepository({http.Client? client})
: _client = client ?? http.Client();
final http.Client _client;
@override
Future<CurrentWeather> getCurrentWeather(City city) async {
final uri = Uri.https('api.open-meteo.com', '/v1/forecast', {
'latitude': '${city.latitude}',
'longitude': '${city.longitude}',
'current': 'temperature_2m,weather_code,wind_speed_10m',
'timezone': 'auto',
});
final response = await _client.get(uri);
if (response.statusCode != 200) {
throw Exception('HTTP-Fehler ${response.statusCode}');
}
final json =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
return ForecastResponse.fromJson(json).current.toCurrentWeather();
}
}ApiWeatherRepository bekommt einen http.Client als optionalen
Parameter, wie das ViewModel sein Repository. as http gibt dem Paket
ein Präfix, damit Namen wie Client eindeutig bleiben. Was der Agent im
Referenz-Repository außerdem geändert hat:
flutter pub add httpträgt das Paket inpubspec.yamlein (http: ^1.6.0) und lädt es herunter.INTERNET-Permission im Android-Manifest, direkt unter dem öffnenden<manifest …>-Tag:
<!-- Im Debug-Build automatisch vorhanden, für die Release-APK nötig -->
<uses-permission android:name="android.permission.INTERNET"/>Ohne diese Zeile funktioniert die App beim Entwickeln trotzdem, weil
Flutter die Permission im Debug-Build automatisch ergänzt
(android/app/src/debug/AndroidManifest.xml). Erst die Release-APK
lädt dann nichts mehr. Ein typischer Fehler, der beim Review auffallen
sollte.
Anders als Ktor wirft http bei HTTP-Fehlern (4xx, 5xx) keine
Exception. Deshalb prüft der Code statusCode selbst.
jsonDecode aus dart:convert macht aus dem JSON-Text eine
Map<String, dynamic>. dynamic heißt: Typ unbekannt, erst zur
Laufzeit geprüft. Die Umwandlung in typisierte Objekte übernehmen die
DTOs. utf8.decode(response.bodyBytes) sorgt dafür, dass Umlaute
richtig ankommen, auch wenn der Server keinen Zeichensatz angibt.
Alternative zu http ist Dio mit mehr
Komfort, z. B. Interceptors. Gute Übung: den Agent fragen, wie dasselbe
mit Dio aussähe, und die beiden Varianten vergleichen.
DTO und Datenmodell
Die JSON-Antwort bekommt eigene Klassen (DTOs). Sie werden auf unser Datenmodell abgebildet.
class CurrentDto {
// const-Konstruktor und final-Felder ...
factory CurrentDto.fromJson(Map<String, dynamic> json) => CurrentDto(
temperature: (json['temperature_2m'] as num).toDouble(),
weatherCode: json['weather_code'] as int,
windSpeed: (json['wind_speed_10m'] as num).toDouble(),
);
}Die DTOs kommen in eine neue Datei im Unterordner data/api.
CurrentDto steckt in einer Hülle ForecastResponse für die gesamte
Antwort, weil die API die Werte im Feld current liefert:
// DTOs bilden die JSON-Antwort von Open-Meteo ab, nicht unser Datenmodell
class ForecastResponse {
const ForecastResponse({required this.current});
factory ForecastResponse.fromJson(Map<String, dynamic> json) =>
ForecastResponse(
current: CurrentDto.fromJson(json['current'] as Map<String, dynamic>),
);
final CurrentDto current;
}
class CurrentDto {
const CurrentDto({
required this.temperature,
required this.weatherCode,
required this.windSpeed,
});
// JSON-Zahlen kommen als num: 14 und 14.2 sind beide möglich
factory CurrentDto.fromJson(Map<String, dynamic> json) => CurrentDto(
temperature: (json['temperature_2m'] as num).toDouble(),
weatherCode: json['weather_code'] as int,
windSpeed: (json['wind_speed_10m'] as num).toDouble(),
);
final double temperature;
final int weatherCode;
final double windSpeed;
}Ein factory-Konstruktor erzeugt das Objekt nicht direkt, sondern gibt
es zurück. fromJson ist die übliche Konvention dafür. Anders als mit
@Serializable in Kotlin schreiben wir die Zuordnung von Hand; JSON-Zahlen
kommen als num (14 oder 14.2), deshalb toDouble(). Das Paket
json_serializable erzeugt
diesen Code automatisch, braucht dafür aber einen Code-Generator.
AGENTS.md schließt das für diese App aus.
Eine Extension im Repository, unter der Klasse ApiWeatherRepository,
bildet das DTO auf das Datenmodell ab:
extension on CurrentDto {
CurrentWeather toCurrentWeather() => CurrentWeather(
temperature: temperature,
windSpeed: windSpeed,
condition: _weatherConditionFromWmoCode(weatherCode),
);
}Eine Extension ohne Namen ist nur in ihrer Datei sichtbar, wie eine
private-Erweiterungsfunktion in Kotlin.
Warum nicht gleich CurrentWeather.fromJson? Weil das Datenmodell dann
vom Format der API abhängt. Mit DTOs bleibt eine Änderung der API auf die
Datenschicht beschränkt; die Oberfläche merkt nichts davon.
Die Wetterlage liefert die API als Zahl (WMO-Code). Ein switch mit
Patterns bildet sie auf WeatherCondition ab. Die Funktion kommt ganz
ans Ende der Datei:
// WMO-Wettercodes, siehe https://open-meteo.com/en/docs (Abschnitt "WMO Weather interpretation codes")
WeatherCondition _weatherConditionFromWmoCode(int code) => switch (code) {
0 => WeatherCondition.clear,
1 || 2 => WeatherCondition.partlyCloudy,
3 => WeatherCondition.cloudy,
45 || 48 => WeatherCondition.fog,
(>= 51 && <= 67) || (>= 80 && <= 82) => WeatherCondition.rain,
(>= 71 && <= 77) || 85 || 86 => WeatherCondition.snow,
>= 95 && <= 99 => WeatherCondition.thunderstorm,
_ => WeatherCondition.cloudy,
};|| verbindet Alternativen, >= 51 && <= 67 ist ein Bereich wie
in 51..67 in Kotlin, _ entspricht else.
Der Nutzen der Architektur
Für echte Daten wurde kein einziges Widget geändert.
$ git diff --stat schritt-08 schritt-09
android/app/src/main/AndroidManifest.xml | 2 ++
lib/data/api/open_meteo_dto.dart | 31 ++++++++++
lib/data/api_weather_repository.dart | 52 ++++++++++++++++
lib/ui/weather_view_model.dart | 4 +-
pubspec.lock | 32 ++++++++++
pubspec.yaml | 1 +Im ViewModel haben sich nur ein Import und der Default geändert:
import '../data/api_weather_repository.dart'; // statt fake_weather_repository.dartWeatherViewModel({WeatherRepository? repository})
: _repository = repository ?? ApiWeatherRepository();pubspec.lock hält die genauen Versionen aller Pakete fest, auch der
indirekten. Die Datei gehört bei Apps ins Repository, damit alle mit
denselben Versionen bauen.
Fehler wie fehlendes Internet landen automatisch im Zustand
WeatherError, weil das ViewModel die Exceptions des Repositorys
bereits abfängt.
Ein Blick beim Review lohnt sich trotzdem: Angezeigt wird '$e', und
das kommt jetzt vom Paket http, z. B. „ClientException with
SocketException: Failed host lookup …“. Das ist Englisch und für Nutzer
unverständlich, obwohl AGENTS.md deutsche Texte verlangt. Am
einfachsten im Emulator mit Flugmodus ausprobieren (siehe Übungen).
Code: schritt-09 ·
Änderungen
Übungen
Selbst oder mit einem Coding Agent umsetzen, jeweils Plan und Diff prüfen.
- Stadtsuche über die Geocoding-API von Open-Meteo
- 7-Tage-Vorhersage im Wetter-Screen
- Button „Erneut versuchen“ bei Fehlern
- Pull-to-Refresh im Wetter-Screen
- Unit-Tests für das ViewModel mit
FakeWeatherRepository - Favoriten: Städte auf dem Gerät speichern
Hinweise zu den Übungen:
- Stadtsuche: Endpunkt
https://geocoding-api.open-meteo.com/v1/search?name=…&count=5&language=de. Die Städteliste wird dafür zu State in einem eigenen ViewModel, undCityListScreenwird zumStatefulWidget, das ViewModel und einenTextEditingControllerfür das Suchfeld besitzt (beide indispose()freigeben). Fehlt das Feldresultsin der Antwort, wurde nichts gefunden. Negative Koordinaten (Süd, West) sollten als „33.87° S“ statt „-33.87° N“ erscheinen. Wer schnell hintereinander sucht, kann Antworten in falscher Reihenfolge bekommen; eine laufende Nummer pro Suche verhindert, dass eine ältere Antwort die neuere überschreibt. Eine neue Stadt kommt mit_cities.add(city)in die Liste; sichtbar wird sie erst nachnotifyListeners(). Damit niemand die Liste annotifyListeners()vorbei ändert, gibt das ViewModel sie nur alsList.unmodifiable(_cities)heraus. Eine Referenzlösung liegt im Tagschritt-11(Änderungen ). Die Dateien zum Kopieren:data/api/geocoding_dto.dart,data/city_repository.dart,data/api_city_repository.dart,ui/city_list_view_model.dart,ui/city_list_screen.dartundmain.dart. Statt einer eigenenUiState-Klasse hält das ViewModel dort mehrere Felder (cities,searchResults,isSearching,errorMessage), weil diese Zustände gleichzeitig auftreten können. Weiterführend, nicht in der Referenzlösung: Mehrere Städte heißen gleich (es gibt mehrere „Hamburg“). Die Felderadmin1(Bundesland) undcountryder Antwort unterscheiden sie in der Trefferliste, dafür brauchtCityweitere Felder. - 7-Tage-Vorhersage: Parameter
daily=weather_code,temperature_2m_max,temperature_2m_min, neues DatenmodellDailyForecast, Liste im Wetter-Screen. - Erneut versuchen:
WeatherContentbekommt einen CallbackonRetry,_WeatherScreenStateübergibt() => _viewModel.loadWeather(widget.city). - Pull-to-Refresh:
RefreshIndicatorum den Inhalt legen;onRefresherwartet einFuture,loadWeatherliefert eines. Der Inhalt muss dafür scrollbar sein, z. B. einListView. - Fehlermeldungen auf Deutsch: Im ViewModel statt
'$e'eigene Texte setzen, z. B. beihttp.ClientException(kein Netz) „Keine Internetverbindung“, sonst „Wetterdaten konnten nicht geladen werden“. - Favoriten: Für wenige Werte reicht
shared_preferences , ein
Key-Value-Speicher für alle Plattformen. Die Liste der Städte wird
dafür z. B. mit
jsonEncodeals String gespeichert und mitjsonDecodewieder gelesen. Für mehr Daten gibt es SQLite, siehe das Cookbook zu SQLite . Pakete mitflutter pub addhinzufügen;AGENTS.mdverlangt, dass ein Agent vorher nachfragt. - Unit-Tests: Tests liegen in
test/und laufen mitflutter test;flutter_testist schon eingebunden.formatTemperaturelässt sich direkt testen. Für das ViewModel mitFakeWeatherRepositorygenügtawait viewModel.loadWeather(city)und danach eine Prüfung vonviewModel.uiState. Einen Main-Dispatcher wie in Android gibt es nicht.