Skip to Content
Mobile Apps4 - FlutterWidget-Grundlagen
Folienvorschau
Dauer
20 Minuten
Ziel
Die Bausteine von Flutter kennenlernen

Themen

  • Projekt in VS Code anlegen
  • Widgets und Hot Reload
  • Column, Row, Container und SizedBox
  • Klassen und enum in Dart, Fake-Daten
  • Scaffold und AppBar
Klicken für Tastaturnavigation · F für Vollbild
Präsentation starten · 20 Minuten ↗

Widget-Grundlagen

Wir bauen die Oberfläche der Wetter-App Schritt für Schritt mit Flutter auf. Am Ende dieser Seite zeigt die App das Wetter einer Stadt mit Fake-Daten an, genau wie die Android-App nach Schritt 4.

Code: Repository auf GitHub , Schritte 0 bis 4.

Die Notizen enthalten den vollständigen Code jeder Datei, sodass sich die App durch Kopieren und Einfügen nachbauen lässt. Die Folien zeigen nur Ausschnitte. Wo auch die Notizen kürzen, verlinken sie die Datei im Repository mit dem Tag des Schritts.

Flutter und Dart

Flutter  ist ein Open-Source-Framework von Google für Apps auf Android, iOS, Web und Desktop aus einer Codebasis. Programmiert wird in Dart .

  • Flutter zeichnet die Oberfläche selbst, statt native UI-Elemente zu verwenden
  • dadurch sieht die App überall fast gleich aus, auch auf alten OS-Versionen
  • Details wie AppBar und Scrollen passt Flutter an die Plattform an
  • Dart wird für Android und iOS in nativen Maschinencode kompiliert

Compose und Flutter verfolgen dasselbe Prinzip: Die Oberfläche wird deklarativ als Baum verschachtelter Bausteine beschrieben. Der Unterschied liegt darunter: Compose erzeugt eine Android-App, Flutter bringt eine eigene Rendering-Engine mit und zeichnet jedes Pixel selbst, auf Android, iOS und im Browser.

Das Aussehen hängt deshalb nicht von der Android- oder iOS-Version ab. Ganz gleich ist die App trotzdem nicht: Flutter passt das Verhalten an die Plattform an. Auf iOS steht der Titel der AppBar mittig, Listen federn am Ende zurück, neue Screens schieben sich von rechts herein und lassen sich per Wischgeste schließen; die Systemschrift ist San Francisco statt Roboto. Wer mehr iOS-Optik möchte, verwendet z. B. Switch.adaptive oder die Cupertino-Widgets. React Native geht den umgekehrten Weg: Es verwendet die nativen UI-Elemente und sieht dadurch automatisch nach der jeweiligen Plattform aus.

Im Oktober 2024 entstand mit Flock  ein Community-Fork von Flutter, angestoßen von Matt Carroll, einem ehemaligen Mitglied des Flutter-Teams (Hintergrund ). Flock sollte Flutter ergänzen und Fixes schneller ausliefern, nicht ersetzen.

Stand Oktober 2026: Im Flock-Repository  gibt es seit Dezember 2025 keine neuen Commits, Releases gab es nie. Für die Vorlesung und das Projekt verwenden wir das Original von Google.

Projekt in VS Code anlegen

Die Flutter-Dokumentation empfiehlt VS Code mit der Flutter-Extension.

  • Extension Flutter installieren (bringt Dart mit)
  • Command Palette → Flutter: New Project → Empty Application
  • Projektname: weather_flutter_tif24
  • Gerät unten rechts in der Statusleiste wählen, starten mit F5

Fehlt das Flutter-SDK, bietet die Extension an, es herunterzuladen (Download SDK). Als Ordner eignet sich ~/develop bzw. C:\Users\<Name>\develop: nicht der Projektordner, kein Cloud-Ordner wie OneDrive, keine Leerzeichen oder Umlaute im Pfad (sonst z. B. C:\develop). Danach Add SDK to PATH bestätigen und VS Code neu starten. Wer einen Paketmanager nutzt, kann Flutter auch darüber installieren, z. B. mit Homebrew unter macOS (brew install --cask flutter), Chocolatey unter Windows (choco install flutter) oder Snap unter Ubuntu (sudo snap install flutter --classic); aktualisiert wird dann ebenfalls über den Paketmanager. flutter doctor im Terminal zeigt, ob alles eingerichtet ist.

Den Android-Emulator kennen wir schon: Er kommt aus dem Device Manager von Android Studio. Android Studio muss dafür installiert sein, aber nicht laufen. In VS Code erscheint der Emulator in der Geräteauswahl (auch über Flutter: Select Device). Ohne Emulator läuft die App auch in Chrome oder als macOS-App.

Projektnamen in Dart bestehen aus Kleinbuchstaben und Unterstrichen. Das Referenz-Repository heißt WeatherFlutterTIF24, das Projekt darin weather_flutter_tif24. Ein Git-Repository legt Flutter nicht an, das geht mit git init im Projektordner.

Android Studio mit dem Flutter-Plugin  funktioniert genauso. Wer dort schon zu Hause ist, kann dabei bleiben. In der Vorlesung verwenden wir VS Code, um zu zeigen, dass Flutter an keine IDE gebunden ist.

Ausgangspunkt (schritt-00) ist die Vorlage Empty Application. In lib/main.dart bleibt nur das Nötigste übrig:

lib/main.dart
import 'package:flutter/material.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 Scaffold(body: SafeArea(child: Placeholder())), ); } }

runApp startet die App mit dem obersten Widget. MaterialApp richtet Theme und Navigation ein, home ist der erste Screen. Placeholder zeichnet ein durchgestrichenes Rechteck, bis dort echter Inhalt steht.

Zusätzlich liegt im Repository eine Datei AGENTS.md mit Regeln für Coding Agents (siehe Navigation und API).

Projektstruktur

Der Dart-Code liegt in lib/, nach Aufgaben in Ordner aufgeteilt.

lib/ ├── main.dart ├── ui/ Screens und Widgets, ViewModels ├── model/ Datenmodell: City, CurrentWeather └── data/ Datenquellen: Fake-Daten, Repositorys └── api/ DTOs für die API (ab Schritt 9) pubspec.yaml Name, Abhängigkeiten, Assets android/ ios/ web/ Plattform-Projekte, selten angefasst

Die Ordner sind dieselben wie in der Android-App. Pfade auf dieser und den folgenden Seiten (z. B. ui/weather_screen.dart) beziehen sich auf lib/. Dateinamen schreibt man in Dart in snake_case, Klassennamen in PascalCase. Importiert wird mit relativen Pfaden, z. B. import '../model/city.dart';.

pubspec.yaml entspricht build.gradle.kts und libs.versions.toml zusammen: Hier stehen die Abhängigkeiten (Pakete von pub.dev ).

Die Ordner android/ und ios/ enthalten echte native Projekte, die Flutter beim Bauen verwendet. Angepasst werden sie nur für Dinge wie Berechtigungen oder App-Icons.

Tipps für VS Code

AktionmacOSWindows/Linux
Quick Fix, z. B. fehlender Import⌘ .Ctrl+.
Code formatieren (Format Document)⇧ ⌥ FShift+Alt+F
Umbenennen, überall (Rename Symbol)F2F2
Zur Definition springen⌘ + KlickCtrl+Klick
Parameter einer Funktion anzeigen⇧ ⌘ LeertasteCtrl+Shift+Leertaste
Command Palette⇧ ⌘ PCtrl+Shift+P

Besonders praktisch: Quick Fix auf einem Widget bietet Wrap with Column, Wrap with Padding, Remove this widget usw. an. Damit lässt sich der Widget-Baum umbauen, ohne Klammern zu zählen.

Snippets sparen Tipparbeit: stless + Tab erzeugt ein StatelessWidget, stful + Tab ein StatefulWidget.

Formatieren beim Speichern: Settings → „Format On Save“ aktivieren.

Widgets

In Flutter ist alles ein Widget: Text, Abstände, Layouts, ganze Screens.

Neue Datei ui/weather_screen.dart:

import 'package:flutter/material.dart'; class WeatherScreen extends StatelessWidget { const WeatherScreen({super.key}); @override Widget build(BuildContext context) { return const Column(children: [Text('Lörrach'), Text('14 °C')]); } }

Ein Widget ist eine Klasse. build beschreibt, wie es aussieht, und gibt genau ein Widget zurück. Mehrere Kinder brauchen deshalb ein Layout wie Column.

Flutter arbeitet wie Compose deklarativ: Wir beschreiben, was angezeigt werden soll. Ändern sich die Daten, ruft Flutter build erneut auf.

ComposeFlutter
Funktion mit @ComposableKlasse, die von StatelessWidget erbt
Parameter der FunktionFelder der Klasse, Konstruktor
Inhalt im FunktionsrumpfRückgabewert von build
Column { … } (Trailing Lambda)Column(children: [ … ])
modifiereigene Widgets und Eigenschaften (siehe unten)

Konventionen für Widgets:

  • const-Konstruktor mit {super.key}: key brauchen wir erst bei Listen, er gehört aber zur Konvention
  • Felder sind final, ein Widget ist unveränderlich
  • const vor einem Widget sagt Flutter, dass es sich nie ändert; es wird nur einmal erzeugt

Damit WeatherScreen erscheint, importiert main.dart die neue Datei und setzt das Widget statt des Placeholder ein:

Code: lib/main.dart (schritt-01)

lib/main.dart
import 'package:flutter/material.dart'; import 'ui/weather_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 Scaffold(body: SafeArea(child: WeatherScreen())), ); } }

SafeArea hält den Inhalt aus Statusleiste und Kamera-Aussparung heraus, wie safeDrawingPadding() in Compose.

Hot Reload: Speichern genügt, die Änderung ist in unter einer Sekunde im laufenden Emulator.

  • Hot Reload (Speichern): Code neu laden, State bleibt erhalten
  • Hot Restart (⇧ ⌘ F5): App neu starten, State geht verloren
  • neuer Build nötig bei Änderungen an pubspec.yaml oder nativem Code

Hot Reload übernimmt die Rolle der @Preview in Android Studio, nur direkt im Emulator und mit echter Interaktion. Am besten ausprobieren: Text ändern, speichern, Ergebnis ansehen.

Manche Änderungen kommen mit Hot Reload nicht an, z. B. neue Startwerte von State oder Änderungen in main(). Dann hilft Hot Restart.

Mit dem Widget Previewer  gibt es auch in Flutter eine Vorschau wie @Preview in Compose: Eine Funktion, die ein Widget zurückgibt, bekommt die Annotation @Preview aus package:flutter/widget_previews.dart. In VS Code erscheint die Vorschau im Tab Flutter Widget Preview in der Seitenleiste. In dieser Lektion verwenden wir Hot Reload.

Code: schritt-01 · Änderungen 

Layouts: Column, Row, Container

Widgets werden mit Layout-Widgets angeordnet.

  • Column: untereinander
  • Row: nebeneinander
  • Stack: übereinander
  • Container: Hintergrund, Rahmen, Abstände um ein Kind
  • SizedBox: fester Abstand, Spacer: freier Platz

Die Verschachtelung im Code entspricht der Verschachtelung auf dem Bildschirm.

Layout als verschachtelte Boxen: Bildschirm und Code haben denselben Aufbau

Column( mainAxisSize: MainAxisSize.min, children: [ Text('Lörrach', style: TextStyle(fontSize: 28)), Text('⛅', style: TextStyle(fontSize: 72)), Text('14 °C', style: TextStyle(fontSize: 48)), SizedBox(height: 16), Row(children: [Text('Wind: 8 km/h'), Spacer(), Text('Teilweise bewölkt')]), ], )

Dieser Code steckt in einem neuen Widget CurrentWeatherCard, ebenfalls in ui/weather_screen.dart. WeatherScreen gibt nur noch const CurrentWeatherCard() zurück. Die vollständige Datei folgt im nächsten Abschnitt, wenn die Karte noch Abstände und Hintergrund bekommt.

Column und Row zentrieren ihre Kinder quer zur Hauptachse standardmäßig. Entlang der Hauptachse nimmt eine Column so viel Platz ein wie möglich; mainAxisSize: MainAxisSize.min macht sie so klein wie ihr Inhalt.

Spacer() nimmt den gesamten freien Platz in der Row ein und schiebt so den zweiten Text an den rechten Rand, wie Spacer(Modifier.weight(1f)) in Compose.

Größen sind logische Pixel, das entspricht dp. Eine eigene Einheit für Schrift wie sp gibt es nicht: Flutter skaliert Text automatisch mit der Schriftgröße aus den Systemeinstellungen.

Im Repository steht die Row über mehrere Zeilen, so wie sie der Formatierer (dart format) anordnet. Seit Dart 3.7 entscheidet er selbst nach der Zeilenlänge, ob ein Aufruf in eine oder mehrere Zeilen kommt, und setzt oder entfernt die abschließenden Kommas passend. Speichern genügt, von Hand formatieren muss man nichts.

Abstände und Hintergrund

Einen Modifier gibt es nicht. Abstände und Hintergrund sind Eigenschaften von Container oder eigene Widgets wie Padding.

Container( width: double.infinity, margin: const EdgeInsets.all(16), // außen padding: const EdgeInsets.all(24), // innen decoration: BoxDecoration( color: const Color(0xFFBBDEFB), borderRadius: BorderRadius.circular(24), ), child: const Column(/* von oben */), )

Anders als bei Modifiern in Compose spielt die Reihenfolge der Argumente keine Rolle: margin ist immer außen, padding immer innen. Dafür wird der Baum tiefer, sobald mehrere Widgets ineinander stecken, z. B. Padding(padding: …, child: Center(child: …)).

width: double.infinity entspricht fillMaxWidth(): so breit wie der verfügbare Platz.

Am besten mit Hot Reload ausprobieren: margin oder padding ändern oder entfernen und den Unterschied ansehen.

Code: lib/ui/weather_screen.dart (schritt-02)

lib/ui/weather_screen.dart
import 'package:flutter/material.dart'; class WeatherScreen extends StatelessWidget { const WeatherScreen({super.key}); @override Widget build(BuildContext context) { return const CurrentWeatherCard(); } } class CurrentWeatherCard extends StatelessWidget { const CurrentWeatherCard({super.key}); @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFFBBDEFB), borderRadius: BorderRadius.circular(24), ), child: const Column( mainAxisSize: MainAxisSize.min, children: [ Text('Lörrach', style: TextStyle(fontSize: 28)), Text('⛅', style: TextStyle(fontSize: 72)), Text('14 °C', style: TextStyle(fontSize: 48)), SizedBox(height: 16), Row( children: [ Text('Wind: 8 km/h'), Spacer(), Text('Teilweise bewölkt'), ], ), ], ), ); } }

Code: schritt-02 · Änderungen 

Datenmodell mit Klassen

Statt fester Texte bekommen Widgets nun Daten über den Konstruktor.

class City { const City({ required this.name, required this.latitude, required this.longitude, }); final String name; final double latitude; final double longitude; }

Der Code oben ist die vollständige Datei model/city.dart im neuen Ordner model.

Eine data class wie in Kotlin gibt es in Dart nicht. Wir schreiben eine normale Klasse mit final-Feldern und einem const-Konstruktor. { … } macht die Parameter zu benannten Parametern, required macht sie zu Pflichtangaben. Aufgerufen wird mit Namen, wie in Kotlin: City(name: 'Lörrach', latitude: 47.6156, longitude: 7.6614). equals und copy erzeugt Dart nicht automatisch; wir brauchen sie hier auch nicht.

CurrentWeather ist genauso aufgebaut. Für die Wetterlage gibt es ein enum, Werte in lowerCamelCase:

lib/model/current_weather.dart
enum WeatherCondition { clear, partlyCloudy, cloudy, fog, rain, snow, thunderstorm, } class CurrentWeather { const CurrentWeather({ required this.temperature, required this.windSpeed, required this.condition, }); final double temperature; final double windSpeed; final WeatherCondition condition; }

Die Koordinaten in City brauchen wir erst in Schritt 9 für die API.

Die Fake-Daten kommen in den neuen Ordner data:

lib/data/fake_data.dart
import '../model/city.dart'; import '../model/current_weather.dart'; const fakeCities = [ City(name: 'Lörrach', latitude: 47.6156, longitude: 7.6614), City(name: 'Freiburg', latitude: 47.9990, longitude: 7.8421), City(name: 'Basel', latitude: 47.5596, longitude: 7.5886), ]; const fakeCurrentWeather = { 'Lörrach': CurrentWeather( temperature: 14.2, windSpeed: 8.5, condition: WeatherCondition.partlyCloudy, ), 'Freiburg': CurrentWeather( temperature: 15.0, windSpeed: 6.1, condition: WeatherCondition.clear, ), 'Basel': CurrentWeather( temperature: 13.6, windSpeed: 11.2, condition: WeatherCondition.rain, ), };

Listen ([…]) und Maps ({…}) haben in Dart eigene Literale. Die Typen List<City> und Map<String, CurrentWeather> leitet Dart ab.

Dart für Kotlin-Kenner: Vieles ist ähnlich, z. B. Null-Safety mit String?, ?. und !, Extension Methods oder String-Templates mit '$name' bzw. '${city.name}'. Unterschiede: Semikolons sind Pflicht, der Typ steht vor dem Namen (final String name), Strings meist in einfachen Anführungszeichen, und _ am Namensanfang macht etwas privat (statt private).

Widgets sollten Daten über den Konstruktor erhalten und sie nicht eigenständig holen.

class CurrentWeatherCard extends StatelessWidget { const CurrentWeatherCard({super.key, required this.city, required this.weather}); final City city; final CurrentWeather weather; @override Widget build(BuildContext context) { // Container und Column wie bisher, die Texte jetzt aus den Daten: // Text(city.name), Text(weather.condition.emoji), ... } }

Weil die Texte jetzt aus Variablen kommen, entfällt das const vor der Column; nur die festen TextStyles bleiben const.

Die Anzeige der Wetterlage ist eine Extension mit einem switch-Ausdruck. Sie gehört zur UI, nicht ins Datenmodell:

lib/ui/weather_condition_ui.dart
import '../model/current_weather.dart'; extension WeatherConditionUi on WeatherCondition { String get emoji => switch (this) { WeatherCondition.clear => '☀️', WeatherCondition.partlyCloudy => '⛅', WeatherCondition.cloudy => '☁️', WeatherCondition.fog => '🌫️', WeatherCondition.rain => '🌧️', WeatherCondition.snow => '❄️', WeatherCondition.thunderstorm => '⛈️', }; String get label => switch (this) { WeatherCondition.clear => 'Sonnig', WeatherCondition.partlyCloudy => 'Teilweise bewölkt', WeatherCondition.cloudy => 'Bewölkt', WeatherCondition.fog => 'Nebel', WeatherCondition.rain => 'Regen', WeatherCondition.snow => 'Schnee', WeatherCondition.thunderstorm => 'Gewitter', }; }

Wie when in Kotlin muss der switch-Ausdruck alle Werte des enum abdecken. Kommt ein neuer Wert hinzu, meldet der Analyzer jede Stelle, die angepasst werden muss. emoji ist ein Getter (get), deshalb steht beim Aufruf keine Klammer: weather.condition.emoji. Nach demselben Muster liefert label den Text („Sonnig“, „Regen“ …).

Auch WeatherScreen bekommt city und weather über den Konstruktor und reicht sie an die Karte weiter:

Code: lib/ui/weather_screen.dart (schritt-03)

lib/ui/weather_screen.dart
import 'package:flutter/material.dart'; import '../model/city.dart'; import '../model/current_weather.dart'; import 'weather_condition_ui.dart'; class WeatherScreen extends StatelessWidget { const WeatherScreen({super.key, required this.city, required this.weather}); final City city; final CurrentWeather weather; @override Widget build(BuildContext context) { return CurrentWeatherCard(city: city, weather: weather); } } class CurrentWeatherCard extends StatelessWidget { const CurrentWeatherCard({ super.key, required this.city, required this.weather, }); final City city; final CurrentWeather weather; @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFFBBDEFB), borderRadius: BorderRadius.circular(24), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(city.name, style: const TextStyle(fontSize: 28)), Text(weather.condition.emoji, style: const TextStyle(fontSize: 72)), Text( '${weather.temperature.round()} °C', style: const TextStyle(fontSize: 48), ), const SizedBox(height: 16), Row( children: [ Text('Wind: ${weather.windSpeed.round()} km/h'), const Spacer(), Text(weather.condition.label), ], ), ], ), ); } }

round() rundet auf eine ganze Zahl, '${…}' setzt einen Ausdruck in einen String ein.

main.dart importiert die Fake-Daten und übergibt sie:

Code: lib/main.dart (schritt-03)

lib/main.dart
import 'package:flutter/material.dart'; import 'data/fake_data.dart'; import 'ui/weather_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: Scaffold( body: SafeArea( child: WeatherScreen( city: fakeCities.first, weather: fakeCurrentWeather[fakeCities.first.name]!, ), ), ), ); } }

Das const vor Scaffold entfällt, weil WeatherScreen jetzt Werte erhält, die erst zur Laufzeit feststehen.

Der Zugriff auf eine Map liefert CurrentWeather?, weil es den Schlüssel vielleicht nicht gibt. ! sagt: Hier ist der Wert sicher da. Fehlt er doch, stürzt die App ab, wie mit getValue in Kotlin.

Code: schritt-03 · Änderungen 

Scaffold und AppBar

Flutter bringt die Widgets von Material Design 3 mit. Scaffold ist das Grundgerüst eines Screens, z. B. mit einer AppBar.

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(city.name)), body: SingleChildScrollView( child: CurrentWeatherCard(city: city, weather: weather), ), ); }

Das Scaffold wandert aus main.dart in WeatherScreen: Der Code oben ersetzt die build-Methode von WeatherScreen in ui/weather_screen.dart, CurrentWeatherCard bleibt unverändert. main.dart setzt WeatherScreen jetzt direkt als home ein, ohne Scaffold und SafeArea. Dies ersetzt den bisherigen Wert von home:

lib/main.dart
home: WeatherScreen( city: fakeCities.first, weather: fakeCurrentWeather[fakeCities.first.name]!, ),

Ein innerPadding wie in Compose gibt es nicht: Das Scaffold legt body automatisch unter die AppBar, und die AppBar berücksichtigt die Statusleiste selbst. Für den oberen Rand ist SafeArea deshalb nicht mehr nötig. Seitliche Aussparungen im Querformat schützt die AppBar nicht; bei Inhalten bis an den Rand hilft dort weiterhin SafeArea.

SingleChildScrollView macht den Inhalt scrollbar, wenn er nicht auf den Bildschirm passt. Im Querformat ist die Karte höher als der freie Platz; ohne Scrollen zeigt Flutter einen gelb-schwarz gestreiften Balken (Overflow). Anders als in Compose scrollt in Flutter nichts von selbst, und zu große Inhalte werden nicht stillschweigend abgeschnitten.

Die AppBar folgt den Konventionen der Plattform: Auf Android steht der Titel links, auf iOS mittig.

Die Karte behält vorerst ihre feste Farbe und Schriftgrößen. Farben und Schriften aus dem Theme, Card und Dark Mode kommen am Ende dazu (siehe Material 3).

Code: schritt-04 · Änderungen 

Übung: Karte gestalten

  • Farben, Abstände und Schriftgrößen der Karte ändern, mit Hot Reload ausprobieren
  • Bonus: neuer Parameter highlighted, der die Karte mit einem Rahmen hervorhebt

Der Aufruf in WeatherScreen soll so aussehen:

CurrentWeatherCard(city: city, weather: weather, highlighted: true)

Ohne highlighted soll die Karte aussehen wie bisher.

Tipps: Ein benannter Parameter ohne required braucht einen Standardwert, z. B. this.highlighted = false. Einen Rahmen zeichnet die BoxDecoration mit border: Border.all(…); null bedeutet kein Rahmen.

Lösung

lib/ui/weather_screen.dart
class CurrentWeatherCard extends StatelessWidget { const CurrentWeatherCard({ super.key, required this.city, required this.weather, this.highlighted = false, }); final City city; final CurrentWeather weather; final bool highlighted; @override Widget build(BuildContext context) { return Container( width: double.infinity, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color(0xFFBBDEFB), borderRadius: BorderRadius.circular(24), border: highlighted ? Border.all(color: const Color(0xFF1565C0), width: 3) : null, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(city.name, style: const TextStyle(fontSize: 28)), Text(weather.condition.emoji, style: const TextStyle(fontSize: 72)), Text( '${weather.temperature.round()} °C', style: const TextStyle(fontSize: 48), ), const SizedBox(height: 16), Row( children: [ Text('Wind: ${weather.windSpeed.round()} km/h'), const Spacer(), Text(weather.condition.label), ], ), ], ), ); } }

Mit this.highlighted = false ist der Parameter optional, wie ein Default-Wert in Kotlin. Das Feld bleibt final bool ohne ?, weil es immer einen Wert hat.

Das Referenz-Repository enthält diese Änderung nicht. Die eingeklappten Dateien auf den folgenden Seiten überschreiben sie; wer sie behalten will, muss sie dort selbst wieder einbauen.

Weiterführendes Material